docs: update README with future plans
19
.env
Normal file
@@ -0,0 +1,19 @@
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.9.2
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||
|
||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||
|
||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
|
||||
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
|
||||
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
|
||||
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
||||
73
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: publish release version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm build+zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
deploy-web:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: build/web
|
||||
create-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/create-release@v1
|
||||
id: create-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
upload-release:
|
||||
needs: [build, create-release]
|
||||
strategy:
|
||||
matrix:
|
||||
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ./build/${{ matrix.client }}.zip
|
||||
asset_name: kiss-translator_${{ github.ref_name }}_${{ matrix.client }}.zip
|
||||
asset_content_type: application/zip
|
||||
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
*.crx
|
||||
*.pem
|
||||
*.zip
|
||||
1
.obsidian/app.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
31
.obsidian/core-plugins.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": true,
|
||||
"webviewer": false
|
||||
}
|
||||
171
.obsidian/workspace.json
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "cbf891568fc78c43",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "d124d5a918f363e6",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "eccb24c47a38046a",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "empty",
|
||||
"state": {},
|
||||
"icon": "lucide-file",
|
||||
"title": "新标签页"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "a206b20e17a47143",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "8eaf7247916eb675",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "acbe859844e3c61f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "文件列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a26def8dfef6d24f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c9eb9713b8d21b0a",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "书签"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300
|
||||
},
|
||||
"right": {
|
||||
"id": "03abd16995f706d4",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "4fc6c366e2698f0a",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "1cdf7d3b425ea21f",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "反向链接"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3e09334caaeb6973",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "出链"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "672281771c8b5c6c",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "标签"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8464d879aaa3512d",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "大纲"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:打开快速切换": false,
|
||||
"graph:查看关系图谱": false,
|
||||
"canvas:新建白板": false,
|
||||
"daily-notes:打开/创建今天的日记": false,
|
||||
"templates:插入模板": false,
|
||||
"command-palette:打开命令面板": false
|
||||
}
|
||||
},
|
||||
"active": "eccb24c47a38046a",
|
||||
"lastOpenFiles": [
|
||||
"README.en.md",
|
||||
"README.md",
|
||||
"kiss-translator.webm",
|
||||
"kiss-translator.png"
|
||||
]
|
||||
}
|
||||
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
public
|
||||
package.json
|
||||
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"experimentalTernaries": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
211
README.en.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# KISS Translator
|
||||
|
||||
English | [简体中文](README.md)
|
||||
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Keep it simple, smart
|
||||
- [x] Open source
|
||||
- [x] Adapt to common browsers
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac)
|
||||
- [x] Thunderbird
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft
|
||||
- [x] Baidu/Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] Custom translation interface
|
||||
- [x] Covers common translation scenarios
|
||||
- [x] Web bilingual translation
|
||||
- [x] Input box translation
|
||||
- [x] Seletction translation
|
||||
- [x] Favorite Words
|
||||
- [x] Mouseover translation
|
||||
- [x] YouTube subtitle translation
|
||||
- [x] Cross-client data synchronization
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] Custom translation rules
|
||||
- [x] Rule subscription/rule sharing
|
||||
- [x] Customized terminology
|
||||
- [x] Custom translation style
|
||||
- [x] Custom shortcut keys
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Setting Popup
|
||||
- `Alt+S` Open Translate Popup / Translate Selected Text
|
||||
- `Alt+O` Open Options Page
|
||||
- `Alt+I` Input Box Translation
|
||||
|
||||
## Install
|
||||
|
||||
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||
>
|
||||
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
|
||||
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
|
||||
|
||||
- [x] Browser extension
|
||||
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [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] Safari (Mac) Compiled by a third party, not verified, obtained by yourself: https://www.nodeloc.com/t/topic/54245
|
||||
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] GreaseMonkey Script
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/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)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## Associated Projects
|
||||
|
||||
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- Data synchronization service available for this project.
|
||||
- Can also be used to share personal private rule lists.
|
||||
- Deploy by yourself, manage by yourself, data is private.
|
||||
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- Provides the latest and most complete list of subscription rules maintained by the community.
|
||||
- Help with rules-related issues.
|
||||
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
|
||||
- Deploy and manage by yourself.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### How to Turn Off Automatic Translation
|
||||
|
||||
You can achieve this through `Rules Setting` with the following methods:
|
||||
|
||||
- Personal Rules: RULES-> Global Rule -> Translate Switch -> Disaabled
|
||||
- Subscription Rules: SUBSCRIBE -> Select the third option `kiss-rules-off.json`
|
||||
- Override Subscription Rules: OVERWRITE -> Translate Switch -> Disaabled
|
||||
- Add a Personal Rule for a Specific Website: Translate Switch -> Disaabled
|
||||
|
||||
### How to Set Keyboard Shortcuts
|
||||
|
||||
Set this in the extension management page, for example:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### How to Turn Off Selection Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> If translate selected -> Disable
|
||||
|
||||
### How to Set it to Show Only the Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> Show Only Translations -> Enable
|
||||
|
||||
### How to Set Mouse Hover Translation
|
||||
|
||||
Set this in the `Rules Setting`: RULES -> Global Rule -> TTrigger Mode
|
||||
|
||||
### Why are some web pages not fully translated?
|
||||
|
||||
This extension's webpage translation is based on CSS selectors. Generic rules cannot adapt to all websites, and sometimes you need to manually add site-specific rules. If you don't know how to write rules, you can seek help here:
|
||||
https://github.com/fishjar/kiss-rules/issues
|
||||
|
||||
### What is the priority order of rule settings?
|
||||
|
||||
Personal Rules > Override Subscription Rules > Subscription Rules > Global Rules
|
||||
|
||||
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||
|
||||
### Why are YouTube subtitles translated in broken sentences?
|
||||
|
||||
This extension has no special development for video content. Support for YouTube is also treated as regular webpage translation. Auto-generated subtitles are streamed and output progressively, resulting in poorer support.
|
||||
|
||||
To disable this extension's subtitle translation, add a rule. Reference: https://github.com/fishjar/kiss-translator/issues/62
|
||||
|
||||
### Local Ollama interface cannot be used
|
||||
|
||||
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### Custom API doesn't work in Tampermonkey scripts
|
||||
|
||||
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||
|
||||
### How to Set Up Hook Functions for Custom Interfaces
|
||||
|
||||
The custom interface feature is highly flexible and can theoretically integrate with any translation interface.
|
||||
|
||||
Example of a Request Hook function:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} text Text to be translated
|
||||
* @param {string} from Source language
|
||||
* @param {string} to Target language
|
||||
* @param {string} url Translation interface URL
|
||||
* @param {string} key Translation interface API key
|
||||
* @returns {Array[string, object]} [Interface URL, request object]
|
||||
*/
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
method: "POST",
|
||||
body: { text, to },
|
||||
}]
|
||||
```
|
||||
|
||||
Example of a Response Hook function:
|
||||
|
||||
```js
|
||||
* Response Hook
|
||||
* @param {string} res JSON data returned by the interface
|
||||
* @param {string} text Text to be translated
|
||||
* @param {string} from Source language
|
||||
* @param {string} to Target language
|
||||
* @returns {Array[string, boolean]} [Translated text, whether target language is same as source]
|
||||
* Note: If the second return value is true (target language same as source),
|
||||
* the translation will not be displayed on the page,
|
||||
* If the parameters are incomplete, it is recommended to return false directly
|
||||
*/
|
||||
(res, text, from, to) => [res.text, to === res.src]
|
||||
```
|
||||
|
||||
For more custom interface examples, refer to: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||
|
||||
## Future Plans
|
||||
|
||||
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:
|
||||
|
||||
- [ ] **Batch Text Requests**: Optimize request strategy to reduce translation API calls and improve performance.
|
||||
- [ ] **Enhanced Rich Text Translation**: Support accurate translation of complex page structures and rich text content.
|
||||
- [ ] **Advanced Custom/AI Interfaces**: Add support for context memory, multi-turn conversations, and other advanced AI features.
|
||||
- [ ] **Fallback English Dictionary**: When translation services fail, fall back to a local dictionary lookup.
|
||||
- [ ] **Improved YouTube Subtitle Support**: Enhance merging and translation experience for streaming subtitles, reducing sentence fragmentation.
|
||||
- [ ] **Upgraded Rule Collaboration System**: Introduce more flexible rule sharing, version management, and community review processes.
|
||||
|
||||
If you're interested in any of these directions, feel free to discuss in [Issues](https://github.com/fishjar/kiss-translator/issues) or submit a PR!
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
git checkout dev # Submit a PR suggestion to push to the dev branch
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Discussion
|
||||
|
||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## Appreciate
|
||||
|
||||

|
||||
210
README.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 简约翻译
|
||||
|
||||
[English](README.en.md) | 简体中文
|
||||
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## 特性
|
||||
|
||||
- [x] 保持简约
|
||||
- [x] 开放源代码
|
||||
- [x] 适配常见浏览器
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [ ] Safari
|
||||
- [x] Safari (Mac)
|
||||
- [x] Thunderbird
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft
|
||||
- [x] Baidu/Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] 自定义翻译接口
|
||||
- [x] 覆盖常见翻译场景
|
||||
- [x] 网页双语对照翻译
|
||||
- [x] 输入框翻译
|
||||
- [x] 划词翻译
|
||||
- [x] 收藏词汇
|
||||
- [x] 鼠标悬停翻译
|
||||
- [x] YouTube 字幕翻译
|
||||
- [x] 跨客户端数据同步
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] 自定义翻译规则
|
||||
- [x] 规则订阅/规则分享
|
||||
- [x] 自定义专业术语
|
||||
- [x] 自定义译文样式
|
||||
- [x] 自定义快捷键
|
||||
- `Alt+Q` 开启翻译
|
||||
- `Alt+C` 切换样式
|
||||
- `Alt+K` 打开设置弹窗
|
||||
- `Alt+S` 打开翻译弹窗/翻译选中文字
|
||||
- `Alt+O` 打开设置页面
|
||||
- `Alt+I` 输入框翻译
|
||||
|
||||
## 安装
|
||||
|
||||
> 注:基于以下原因,建议优先使用浏览器扩展
|
||||
>
|
||||
> - 浏览器扩展的功能更完整(本地语言识别、右键菜单等)
|
||||
> - 油猴脚本会遇到更多使用上的问题(跨域问题、脚本冲突等)
|
||||
|
||||
- [x] 浏览器扩展
|
||||
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [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] Safari (Mac) 第三方编译,未作验证,自行获取: https://www.nodeloc.com/t/topic/54245
|
||||
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] 油猴脚本
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/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)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## 关联项目
|
||||
|
||||
- 数据同步服务: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- 可用于本项目的数据同步服务。
|
||||
- 亦可用于分享个人的私有规则列表。
|
||||
- 自己部署,自己管理,数据私有。
|
||||
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- 提供社区维护的,最新最全的订阅规则列表。
|
||||
- 求助规则相关的问题。
|
||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
|
||||
- 自己部署,自己管理。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何关闭自动翻译
|
||||
|
||||
通过规则设置,以下方法均可实现:
|
||||
|
||||
- 个人规则:全局规则 -> 开启翻译 -> 默认关闭
|
||||
- 订阅规则:选择第三个 `kiss-rules-off.json`
|
||||
- 覆写订阅规则:开启翻译 -> 默认关闭
|
||||
- 添加一条针对某个网站的个人规则:开启翻译 -> 默认关闭
|
||||
|
||||
### 如何设置快捷键
|
||||
|
||||
在插件管理那里设置,例如:
|
||||
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
### 如何关闭划词翻译
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 是否启用划词翻译 -> 禁用
|
||||
|
||||
### 如何设置仅显示译文
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 仅显示译文 -> 启用
|
||||
|
||||
### 如何设置鼠标悬停翻译
|
||||
|
||||
通过规则设置:个人规则 -> 全局规则 -> 触发方式
|
||||
|
||||
### 为什么有些网页翻译不全
|
||||
|
||||
本插件的网页翻译是基于CSS选择器的,通用规则不能适配所有网页,有时需要自行添加相应网站的单独规则。如果不会写规则,可以到这里求助: https://github.com/fishjar/kiss-rules/issues
|
||||
|
||||
### 规则设置的优先级是如何的
|
||||
|
||||
个人规则 > 覆写订阅规则 > 订阅规则 > 全局规则
|
||||
|
||||
其中全局规则优先级最低,但非常重要,相当于默认规则。
|
||||
|
||||
### 为什么油管字幕一句话会断开翻译
|
||||
|
||||
本插件目前没有针对视频做特殊开发,对油管的支持也是当做网页翻译看待,自动生成字幕是流式生成并输出的,所以支持较差。
|
||||
|
||||
如果需要关闭本插件的字幕翻译,增加一条规则即可,参考:https://github.com/fishjar/kiss-translator/issues/62
|
||||
|
||||
### 本地的Ollama接口不能使用
|
||||
|
||||
如果出现403的情况,参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 填写的接口在油猴脚本不能使用
|
||||
|
||||
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||
|
||||
### 如何设置自定义接口的hook函数
|
||||
|
||||
自定义接口功能非常灵活,理论可以接入任何翻译接口。
|
||||
|
||||
Request Hook 函数示例如下:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} text 需要翻译的原文
|
||||
* @param {string} from 原文语言
|
||||
* @param {string} to 译文语言
|
||||
* @param {string} url 翻译接口地址
|
||||
* @param {string} key 翻译接口密钥
|
||||
* @returns {Array[string, object]} [接口地址, 请求参数对象]
|
||||
*/
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
method: "POST",
|
||||
body: { text, to },
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook 函数示例如下:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Request Hook
|
||||
* @param {string} res 接口返回的json数据
|
||||
* @param {string} text 需要翻译的原文
|
||||
* @param {string} from 原文语言
|
||||
* @param {string} to 译文语言
|
||||
* @returns {Array[string, boolean]} [译文, 译文语言与原文语言是否相同]
|
||||
* 注:如果返回值第二个值为true(译文语言与原文语言相同)则译文不会在页面显示,
|
||||
* 参数不全的情况建议直接返回false
|
||||
*/
|
||||
(res, text, from, to) => [res.text, to === res.src]
|
||||
```
|
||||
|
||||
更多的自定义接口示例,请参考: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||
|
||||
## 未来规划
|
||||
|
||||
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:
|
||||
|
||||
- [ ] **聚合发送文本**:优化请求策略,减少翻译接口调用次数,提升性能。
|
||||
- [ ] **增强富文本翻译**:支持更复杂的页面结构和富文本内容的准确翻译。
|
||||
- [ ] **强化自定义/AI 接口**:支持上下文记忆、多轮对话等高级 AI 功能。
|
||||
- [ ] **英文词典备灾机制**:当翻译服务失效时,可 fallback 到本地词典查询。
|
||||
- [ ] **优化 YouTube 字幕支持**:改进流式字幕的合并与翻译体验,减少断句。
|
||||
- [ ] **规则共建机制升级**:引入更灵活的规则分享、版本管理与社区评审流程。
|
||||
|
||||
如果你对某个方向感兴趣,欢迎在 [Issues](https://github.com/fishjar/kiss-translator/issues) 中讨论或提交 PR!
|
||||
|
||||
## 开发指引
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
git checkout dev # 提交PR建议推送到dev分支
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 交流
|
||||
|
||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## 赞赏
|
||||
|
||||

|
||||
218
config-overrides.js
Normal file
@@ -0,0 +1,218 @@
|
||||
const paths = require("react-scripts/config/paths");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const webpack = require("webpack");
|
||||
|
||||
console.log("process.env.REACT_APP_CLIENT", process.env.REACT_APP_CLIENT);
|
||||
|
||||
// 扩展
|
||||
const extWebpack = (config, env) => {
|
||||
const isEnvProduction = env === "production";
|
||||
const minify = isEnvProduction && {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
};
|
||||
const names = [
|
||||
"HtmlWebpackPlugin",
|
||||
"WebpackManifestPlugin",
|
||||
"MiniCssExtractPlugin",
|
||||
];
|
||||
|
||||
config.entry = {
|
||||
popup: paths.appSrc + "/popup.js",
|
||||
options: paths.appSrc + "/options.js",
|
||||
background: paths.appSrc + "/background.js",
|
||||
content: paths.appSrc + "/content.js",
|
||||
};
|
||||
|
||||
config.output.filename = "[name].js";
|
||||
config.output.assetModuleFilename = "media/[name][ext]";
|
||||
config.optimization.splitChunks = { cacheGroups: { default: false } };
|
||||
config.optimization.runtimeChunk = false;
|
||||
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !names.includes(plugin.constructor.name)
|
||||
);
|
||||
|
||||
config.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["options"],
|
||||
template: paths.appHtml,
|
||||
filename: "options.html",
|
||||
minify,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["popup"],
|
||||
template: paths.appHtml,
|
||||
filename: "popup.html",
|
||||
minify,
|
||||
}),
|
||||
new WebpackManifestPlugin({
|
||||
fileName: "asset-manifest.json",
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/[name].css",
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 油猴
|
||||
const userscriptWebpack = (config, env) => {
|
||||
const banner = `// ==UserScript==
|
||||
// @name ${process.env.REACT_APP_NAME}
|
||||
// @namespace ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @version ${process.env.REACT_APP_VERSION}
|
||||
// @description A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
|
||||
// @author Gabe<yugang2002@gmail.com>
|
||||
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @license GPL-3.0
|
||||
// @match *://*/*
|
||||
// @icon ${process.env.REACT_APP_LOGOURL}
|
||||
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM.setValue
|
||||
// @grant GM.getValue
|
||||
// @grant GM.deleteValue
|
||||
// @grant GM.info
|
||||
// @grant unsafeWindow
|
||||
// @connect translate.googleapis.com
|
||||
// @connect translate-pa.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
// @connect edge.microsoft.com
|
||||
// @connect api-free.deepl.com
|
||||
// @connect api.deepl.com
|
||||
// @connect www2.deepl.com
|
||||
// @connect api.openai.com
|
||||
// @connect generativelanguage.googleapis.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
// @connect github.io
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @connect dav.jianguoyun.com
|
||||
// @connect fanyi.baidu.com
|
||||
// @connect transmart.qq.com
|
||||
// @connect niutrans.com
|
||||
// @connect translate.volcengine.com
|
||||
// @connect localhost
|
||||
// @connect 127.0.0.1
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
`;
|
||||
|
||||
const names = ["HtmlWebpackPlugin"];
|
||||
|
||||
config.entry = {
|
||||
main: paths.appIndexJs,
|
||||
options: paths.appSrc + "/options.js",
|
||||
"kiss-translator.user": paths.appSrc + "/userscript.js",
|
||||
};
|
||||
|
||||
config.output.filename = "[name].js";
|
||||
config.output.publicPath = env === "production" ? "./" : "/";
|
||||
config.optimization.splitChunks = { cacheGroups: { default: false } };
|
||||
config.optimization.runtimeChunk = false;
|
||||
config.optimization.minimize = false;
|
||||
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !names.includes(plugin.constructor.name)
|
||||
);
|
||||
|
||||
config.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["main"],
|
||||
template: paths.appHtml,
|
||||
filename: "index.html",
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["options"],
|
||||
template: paths.appHtml,
|
||||
filename: "options.html",
|
||||
}),
|
||||
new webpack.BannerPlugin({
|
||||
banner,
|
||||
raw: true,
|
||||
entryOnly: true,
|
||||
include: "kiss-translator.user",
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 开发
|
||||
const webWebpack = (config, env) => {
|
||||
const names = ["HtmlWebpackPlugin"];
|
||||
|
||||
config.entry = {
|
||||
main: paths.appIndexJs,
|
||||
options: paths.appSrc + "/options.js",
|
||||
content: paths.appSrc + "/userscript.js",
|
||||
};
|
||||
|
||||
config.output.filename = "[name].js";
|
||||
config.output.publicPath = "/";
|
||||
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !names.includes(plugin.constructor.name)
|
||||
);
|
||||
|
||||
config.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["main"],
|
||||
template: paths.appHtml,
|
||||
filename: "index.html",
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["options"],
|
||||
template: paths.appHtml,
|
||||
filename: "options.html",
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
chunks: ["content"],
|
||||
template: paths.appPublic + "/content.html",
|
||||
filename: "content.html",
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
let webpackConfig;
|
||||
switch (process.env.REACT_APP_CLIENT) {
|
||||
case "userscript":
|
||||
webpackConfig = userscriptWebpack;
|
||||
break;
|
||||
case "web":
|
||||
webpackConfig = webWebpack;
|
||||
break;
|
||||
default:
|
||||
webpackConfig = extWebpack;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
webpack: webpackConfig,
|
||||
};
|
||||
346
custom-api.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# 自定义接口示例
|
||||
|
||||
以下示例为网友提供,仅供学习参考。
|
||||
|
||||
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||
|
||||
> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
http://localhost:8000/v1/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
// 模型支持的语言代码到完整名称的映射
|
||||
const langFullNameMap = {
|
||||
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
|
||||
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
|
||||
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
|
||||
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
|
||||
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
|
||||
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
|
||||
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
|
||||
};
|
||||
|
||||
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
|
||||
const getModelLangCode = (lang) => {
|
||||
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
|
||||
return lang;
|
||||
};
|
||||
|
||||
const sourceLangCode = getModelLangCode(from);
|
||||
const targetLangCode = getModelLangCode(to);
|
||||
|
||||
const sourceLangName = langFullNameMap[sourceLangCode] || from;
|
||||
const targetLangName = langFullNameMap[targetLangCode] || to;
|
||||
|
||||
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
|
||||
|
||||
// 构建请求体对象
|
||||
const bodyObject = {
|
||||
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
|
||||
prompt: prompt,
|
||||
max_tokens: 2048,
|
||||
temperature: 0.0,
|
||||
};
|
||||
|
||||
// 返回最终的请求配置
|
||||
return [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
|
||||
body: JSON.stringify(bodyObject),
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => {
|
||||
// 检查返回是否有效
|
||||
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
|
||||
|
||||
// 提取译文并去除可能存在的前后空格
|
||||
const translatedText = res.choices[0].text.trim();
|
||||
|
||||
// 比较原文与译文,相同为 true,否则为 false。
|
||||
const areTextsIdentical = text.trim() === translatedText;
|
||||
|
||||
// 返回数组:[翻译后的文本, 是否与原文相同]
|
||||
return [translatedText, areTextsIdentical];
|
||||
}
|
||||
// 如果响应格式不正确或没有结果,则抛出错误
|
||||
throw new Error("Invalid API response format or no translation found.");
|
||||
}
|
||||
```
|
||||
|
||||
## 接入 openrouter
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://openrouter.ai/api/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${key}`,
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": //可自定义你的提示词
|
||||
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
|
||||
|
||||
## Translation Rules
|
||||
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
|
||||
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
|
||||
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
|
||||
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
|
||||
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
|
||||
6. Maintain the same level of formality or informality as the original.
|
||||
|
||||
Source Text: ${text}
|
||||
|
||||
Translated Text:`
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.choices?.[0]?.message?.content ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://generativelanguage.googleapis.com/v1beta/models
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"generationConfig": {
|
||||
"temperature": 0.8,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
|
||||
},
|
||||
},
|
||||
"safetySettings": [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
}
|
||||
],
|
||||
"contents": [{
|
||||
"parts": [{
|
||||
"text": `自定义提示词`
|
||||
}]
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 Qwen-MT
|
||||
|
||||
> 由网友 atom 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
const mapLanguageCode = (lang) => ({
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'zh_tw',
|
||||
})[lang] || lang;
|
||||
|
||||
const targetLang = mapLanguageCode(to);
|
||||
|
||||
return [
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "qwen-mt-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": text
|
||||
}
|
||||
],
|
||||
"translation_options": {
|
||||
"source_lang": "auto",
|
||||
"target_lang": targetLang
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
|
||||
```
|
||||
|
||||
|
||||
## 接入 deepl 接口
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
target_lang: "ZH",
|
||||
source_lang: "auto",
|
||||
}),
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
|
||||
```
|
||||
|
||||
## 接入智谱AI大模型
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": key
|
||||
},
|
||||
"body": JSON.stringify({
|
||||
"model": "glm-4-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role":"system",
|
||||
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
## 接入谷歌新接口
|
||||
|
||||
> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://translate-pa.googleapis.com/v1/translateHtml
|
||||
```
|
||||
|
||||
KEY
|
||||
|
||||
```sh
|
||||
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json+protobuf",
|
||||
"X-Goog-API-Key": key
|
||||
},
|
||||
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
|
||||
```
|
||||
|
||||
|
||||
BIN
kiss-translator.png
Normal file
|
After Width: | Height: | Size: 399 KiB |
BIN
kiss-translator.webm
Normal file
74
package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.9.2",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.15",
|
||||
"@mui/lab": "5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.15",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"sval": "^0.5.2",
|
||||
"webdav": "^5.3.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_CLIENT=web react-app-rewired start",
|
||||
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
|
||||
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
|
||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
||||
"build:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.json",
|
||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
|
||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||
"build:rules": "babel-node src/rules.js",
|
||||
"build": "pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
|
||||
"build+zip": "pnpm build && pnpm zip",
|
||||
"format": "prettier --write \"**/*.{js,json,html}\"",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"globals": {
|
||||
"GM": true,
|
||||
"unsafeWindow": true,
|
||||
"globalThis": true,
|
||||
"messenger": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.20",
|
||||
"@babel/node": "^7.22.19",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"prettier": "3.6.2",
|
||||
"react-app-rewired": "^2.2.1"
|
||||
}
|
||||
}
|
||||
13020
pnpm-lock.yaml
generated
Normal file
0
public/.nojekyll
Normal file
20
public/_locales/en/messages.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app_name": {
|
||||
"message": "KISS Translator"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "A simple bilingual translation extension & Greasemonkey script"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "Toggle Translate"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "Open Options"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "Translate Popup/Selected"
|
||||
}
|
||||
}
|
||||
20
public/_locales/zh_CN/messages.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app_name": {
|
||||
"message": "简约翻译"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一个简约的双语对照翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "开启翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "打开设置"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "翻译弹窗/选中文字"
|
||||
}
|
||||
}
|
||||
334
public/content.html
Normal file
@@ -0,0 +1,334 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>%REACT_APP_NAME%</title>
|
||||
<style>
|
||||
img {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
svg {
|
||||
max-width: 1.2em;
|
||||
max-height: 1.2em;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// (() => {
|
||||
// var shadow = document.querySelector("#shadow1");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// root.appendChild(newLine);
|
||||
// })();
|
||||
|
||||
// setTimeout(function () {
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
// }, 1000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("p");
|
||||
// newLine.innerText = "new line";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 2000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// var newLine = document.createElement("div");
|
||||
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
|
||||
// var shadow = document.querySelector("#shadow2");
|
||||
// shadow.shadowRoot.appendChild(newLine);
|
||||
// }, 3000);
|
||||
|
||||
// setTimeout(function () {
|
||||
// var el = document.querySelector("h2");
|
||||
// el.innerText = "hello world";
|
||||
|
||||
// var title = document.querySelector("#addtitle");
|
||||
// title.innerHTML =
|
||||
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
|
||||
// }, 1000);
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.querySelector('h2>p>span');
|
||||
el.innerText = 'hello world';
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div id="content">
|
||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is coming to
|
||||
Google Cloud with the C3A instances.
|
||||
<br />
|
||||
But these upcoming instances for now are only in private preview form.
|
||||
<br />
|
||||
<br />
|
||||
Needless to say I also haven't had any AmpereOne access to check out the
|
||||
performance and power efficiency of these new Arm server processors from Ampere
|
||||
Computing.
|
||||
<br />
|
||||
</div>
|
||||
<h2>
|
||||
<p>
|
||||
<span>React is a JavaScript library for building user interfaces.</span>
|
||||
</p>
|
||||
</h2>
|
||||
<hr />
|
||||
<input id="input1" style="width: 80%" />
|
||||
<hr />
|
||||
<textarea id="textarea1" style="width: 80%">test</textarea>
|
||||
<hr />
|
||||
<div id="addtitle"></div>
|
||||
<h2>Shadow 1</h2>
|
||||
<div id="shadow1"></div>
|
||||
<h2>Shadow 2</h2>
|
||||
<div id="shadow2"></div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
React Server Components (or RSC) is a new application architecture designed by the
|
||||
React team.
|
||||
</h2>
|
||||
<iframe
|
||||
id="iframe1"
|
||||
width="800px"
|
||||
height="600px"
|
||||
src="http://localhost:3000/index.html"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>We’ve first shared our research on RSC in an introductory talk and an RFC.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
To recap them, we are introducing a new kind of component—Server Components—that
|
||||
run ahead of time and are excluded from your JavaScript bundle.
|
||||
</h2>
|
||||
<iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont1">
|
||||
<h2>
|
||||
Server Components can run during the build, letting you read from the filesystem
|
||||
or fetch static content.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
They can also run on the server, letting you access your data layer without
|
||||
having to build an API. You can pass data by props from Server Components to
|
||||
the interactive Client Components in the browser.
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="cont cont2">
|
||||
<h2>
|
||||
Since our last update, we have merged the React Server Components RFC to ratify
|
||||
the proposal.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
RSC combines the simple “request/response” mental model of server-centric
|
||||
Multi-Page Apps with the seamless interactivity of client-centric Single-Page
|
||||
Apps, giving you the best of both worlds.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
||||
能高效更新并渲染合适的组件。
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/logo128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/logo16.png
Normal file
|
After Width: | Height: | Size: 469 B |
BIN
public/images/logo192.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/images/logo32.png
Normal file
|
After Width: | Height: | Size: 898 B |
BIN
public/images/logo48.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
22
public/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>%REACT_APP_NAME%</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
71
public/manifest.firefox.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
"48": "images/logo48.png",
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"default_title": "__MSG_app_name__",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
67
public/manifest.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
"48": "images/logo48.png",
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"default_title": "__MSG_app_name__",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
78
public/manifest.thunderbird.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.9.2",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "yugang2002@gmail.com",
|
||||
"strict_min_version": "78.0"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"storage",
|
||||
"menus",
|
||||
"messagesModify",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
"48": "images/logo48.png",
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"default_title": "__MSG_app_name__",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
BIN
screenshot-01.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
screenshot-02.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
23
src/apis/baidu.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import queryString from "query-string";
|
||||
import { URL_BAIDU_TRANSAPI, DEFAULT_USER_AGENT } from "../config";
|
||||
|
||||
export const genBaidu = async ({ text, from, to }) => {
|
||||
const data = {
|
||||
from,
|
||||
to,
|
||||
query: text,
|
||||
source: "txt",
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
// Origin: "https://fanyi.baidu.com",
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"User-Agent": DEFAULT_USER_AGENT,
|
||||
},
|
||||
method: "POST",
|
||||
body: queryString.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_BAIDU_TRANSAPI, init];
|
||||
};
|
||||
58
src/apis/deepl.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { URL_DEEPLFREE_TRAN } from "../config";
|
||||
|
||||
let id = 1e4 * Math.round(1e4 * Math.random());
|
||||
|
||||
export const genDeeplFree = ({ text, from, to }) => {
|
||||
const iCount = (text.match(/[i]/g) || []).length + 1;
|
||||
let timestamp = Date.now();
|
||||
timestamp = timestamp + (iCount - (timestamp % iCount));
|
||||
id++;
|
||||
|
||||
let body = JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "LMT_handle_texts",
|
||||
params: {
|
||||
splitting: "newlines",
|
||||
lang: {
|
||||
target_lang: to,
|
||||
source_lang_user_selected: from,
|
||||
},
|
||||
commonJobParams: {
|
||||
wasSpoken: false,
|
||||
transcribe_as: "",
|
||||
},
|
||||
id,
|
||||
timestamp,
|
||||
texts: [
|
||||
{
|
||||
text,
|
||||
requestAlternatives: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
body = body.replace(
|
||||
'method":"',
|
||||
(id + 3) % 13 === 0 || (id + 5) % 29 === 0 ? 'method" : "' : 'method": "'
|
||||
);
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "*/*",
|
||||
"x-app-os-name": "iOS",
|
||||
"x-app-os-version": "16.3.0",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"x-app-device": "iPhone13,2",
|
||||
"User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
|
||||
"x-app-build": "510265",
|
||||
"x-app-version": "2.9.1",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
};
|
||||
|
||||
return [URL_DEEPLFREE_TRAN, init];
|
||||
};
|
||||
366
src/apis/index.js
Normal file
@@ -0,0 +1,366 @@
|
||||
import queryString from "query-string";
|
||||
import { fetchData } from "../libs/fetch";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
URL_CACHE_TRAN,
|
||||
KV_SALT_SYNC,
|
||||
URL_GOOGLE_TRAN,
|
||||
URL_MICROSOFT_LANGDETECT,
|
||||
URL_BAIDU_LANGDETECT,
|
||||
URL_BAIDU_SUGGEST,
|
||||
URL_BAIDU_TTS,
|
||||
OPT_LANGS_BAIDU,
|
||||
URL_TENCENT_TRANSMART,
|
||||
OPT_LANGS_TENCENT,
|
||||
OPT_LANGS_SPECIAL,
|
||||
OPT_LANGS_MICROSOFT,
|
||||
} from "../config";
|
||||
import { sha256 } from "../libs/utils";
|
||||
import interpreter from "../libs/interpreter";
|
||||
import { msAuth } from "../libs/auth";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 同步数据
|
||||
* @param {*} url
|
||||
* @param {*} key
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
fetchData(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载数据
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const apiFetch = (url) => fetchData(url);
|
||||
|
||||
/**
|
||||
* Google语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiGoogleLangdetect = async (text) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: "auto",
|
||||
tl: "zh-CN",
|
||||
q: text,
|
||||
};
|
||||
const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
|
||||
const res = await fetchData(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return res.src;
|
||||
};
|
||||
|
||||
/**
|
||||
* Microsoft语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiMicrosoftLangdetect = async (text) => {
|
||||
const [token] = await msAuth();
|
||||
const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiBaiduLangdetect = async (text) => {
|
||||
const res = await fetchData(URL_BAIDU_LANGDETECT, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
query: text,
|
||||
}),
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
if (res.error === 0) {
|
||||
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度翻译建议
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiBaiduSuggest = async (text) => {
|
||||
const res = await fetchData(URL_BAIDU_SUGGEST, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
kw: text,
|
||||
}),
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
if (res.errno === 0) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度语音
|
||||
* @param {*} text
|
||||
* @param {*} lan
|
||||
* @param {*} spd
|
||||
* @returns
|
||||
*/
|
||||
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
|
||||
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
|
||||
return fetchData(url, {
|
||||
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 腾讯语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiTencentLangdetect = async (text) => {
|
||||
const body = JSON.stringify({
|
||||
header: {
|
||||
fn: "text_analysis",
|
||||
},
|
||||
text,
|
||||
});
|
||||
|
||||
const res = await fetchData(URL_TENCENT_TRANSMART, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
useCache: true,
|
||||
});
|
||||
|
||||
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一翻译接口
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const apiTranslate = async ({
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting = {},
|
||||
useCache = true,
|
||||
usePool = true,
|
||||
}) => {
|
||||
let trText = "";
|
||||
let isSame = false;
|
||||
|
||||
if (!text) {
|
||||
return [trText, true];
|
||||
}
|
||||
|
||||
const from =
|
||||
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
||||
if (!to) {
|
||||
kissLog(`target lang: ${toLang} not support`, "translate");
|
||||
return [trText, isSame];
|
||||
}
|
||||
|
||||
// 版本号一/二位升级,旧缓存失效
|
||||
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
||||
const cacheOpts = {
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
version: [v1, v2].join("."),
|
||||
};
|
||||
|
||||
const transOpts = {
|
||||
translator,
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
|
||||
const res = await fetchData(
|
||||
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
|
||||
{
|
||||
useCache,
|
||||
usePool,
|
||||
transOpts,
|
||||
apiSetting,
|
||||
}
|
||||
);
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
isSame = to === res.src;
|
||||
break;
|
||||
case OPT_TRANS_GOOGLE_2:
|
||||
trText = res?.[0]?.[0] || "";
|
||||
isSame = to === res.src;
|
||||
break;
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
trText = res
|
||||
.map((item) => item.translations.map((item) => item.text).join(" "))
|
||||
.join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_DEEPL:
|
||||
trText = res.translations.map((item) => item.text).join(" ");
|
||||
isSame = to === res.translations[0].detected_source_language;
|
||||
break;
|
||||
case OPT_TRANS_DEEPLFREE:
|
||||
trText = res.result?.texts.map((item) => item.text).join(" ");
|
||||
isSame = to === res.result?.lang;
|
||||
break;
|
||||
case OPT_TRANS_DEEPLX:
|
||||
trText = res.data;
|
||||
isSame = to === res.source_lang;
|
||||
break;
|
||||
case OPT_TRANS_NIUTRANS:
|
||||
const json = JSON.parse(res);
|
||||
if (json.error_msg) {
|
||||
throw new Error(json.error_msg);
|
||||
}
|
||||
trText = json.tgt_text;
|
||||
isSame = to === json.from;
|
||||
break;
|
||||
case OPT_TRANS_BAIDU:
|
||||
// trText = res.trans_result?.data.map((item) => item.dst).join(" ");
|
||||
// isSame = res.trans_result?.to === res.trans_result?.from;
|
||||
if (res.type === 1) {
|
||||
trText = Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0];
|
||||
isSame = to === res.from;
|
||||
} else if (res.type === 2) {
|
||||
trText = res.data.map((item) => item.dst).join(" ");
|
||||
isSame = to === res.from;
|
||||
}
|
||||
break;
|
||||
case OPT_TRANS_TENCENT:
|
||||
trText = res?.auto_translation?.[0];
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_VOLCENGINE:
|
||||
trText = res?.translation || "";
|
||||
isSame = to === res?.detected_language;
|
||||
break;
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
trText = res?.choices?.map((item) => item.message.content).join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_GEMINI:
|
||||
trText = res?.candidates
|
||||
?.map((item) => item.content?.parts.map((item) => item.text).join(" "))
|
||||
.join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CLAUDE:
|
||||
trText = res?.content?.map((item) => item.text).join(" ");
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
trText = res?.result?.translated_text;
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
const { thinkIgnore = "" } = apiSetting;
|
||||
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
|
||||
if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||
trText = res?.response.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||
} else {
|
||||
trText = res?.response;
|
||||
}
|
||||
isSame = text === trText;
|
||||
break;
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
case OPT_TRANS_CUSTOMIZE_4:
|
||||
case OPT_TRANS_CUSTOMIZE_5:
|
||||
const { resHook } = apiSetting;
|
||||
if (resHook?.trim()) {
|
||||
interpreter.run(`exports.resHook = ${resHook}`);
|
||||
[trText, isSame] = interpreter.exports.resHook(res, text, from, to);
|
||||
} else {
|
||||
trText = res.text;
|
||||
isSame = to === res.from;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return [trText, isSame, res];
|
||||
};
|
||||
612
src/apis/trans.js
Normal file
@@ -0,0 +1,612 @@
|
||||
import queryString from "query-string";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
URL_MICROSOFT_TRAN,
|
||||
URL_TENCENT_TRANSMART,
|
||||
URL_VOLCENGINE_TRAN,
|
||||
INPUT_PLACE_URL,
|
||||
INPUT_PLACE_FROM,
|
||||
INPUT_PLACE_TO,
|
||||
INPUT_PLACE_TEXT,
|
||||
INPUT_PLACE_KEY,
|
||||
INPUT_PLACE_MODEL,
|
||||
} from "../config";
|
||||
import { msAuth } from "../libs/auth";
|
||||
import { genDeeplFree } from "./deepl";
|
||||
import { genBaidu } from "./baidu";
|
||||
import interpreter from "../libs/interpreter";
|
||||
|
||||
const keyMap = new Map();
|
||||
const urlMap = new Map();
|
||||
|
||||
// 轮询key/url
|
||||
const keyPick = (translator, key = "", cacheMap) => {
|
||||
const keys = key
|
||||
.split(/\n|,/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const preIndex = cacheMap.get(translator) ?? -1;
|
||||
const curIndex = (preIndex + 1) % keys.length;
|
||||
cacheMap.set(translator, curIndex);
|
||||
|
||||
return keys[curIndex];
|
||||
};
|
||||
|
||||
const genGoogle = ({ text, from, to, url, key }) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: from,
|
||||
tl: to,
|
||||
q: text,
|
||||
};
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genGoogle2 = ({ text, from, to, url, key }) => {
|
||||
const body = JSON.stringify([[[text], from, to], "wt_lib"]);
|
||||
const init = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json+protobuf",
|
||||
"X-Goog-API-Key": key,
|
||||
},
|
||||
body,
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genMicrosoft = async ({ text, from, to }) => {
|
||||
const [token] = await msAuth();
|
||||
const params = {
|
||||
from,
|
||||
to,
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
};
|
||||
|
||||
return [input, init];
|
||||
};
|
||||
|
||||
const genDeepl = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text: [text],
|
||||
target_lang: to,
|
||||
source_lang: from,
|
||||
// split_sentences: "0",
|
||||
};
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `DeepL-Auth-Key ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genDeeplX = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text,
|
||||
target_lang: to,
|
||||
source_lang: from,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
|
||||
const data = {
|
||||
from,
|
||||
to,
|
||||
apikey: key,
|
||||
src_text: text,
|
||||
dictNo,
|
||||
memoryNo,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genTencent = ({ text, from, to }) => {
|
||||
const data = {
|
||||
header: {
|
||||
fn: "auto_translation",
|
||||
client_key:
|
||||
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
|
||||
},
|
||||
type: "plain",
|
||||
model_category: "normal",
|
||||
source: {
|
||||
text_list: [text],
|
||||
lang: from,
|
||||
},
|
||||
target: {
|
||||
lang: to,
|
||||
},
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||
referer: "https://transmart.qq.com/zh-CN/index",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_TENCENT_TRANSMART, init];
|
||||
};
|
||||
|
||||
const genVolcengine = ({ text, from, to }) => {
|
||||
const data = {
|
||||
source_language: from,
|
||||
target_language: to,
|
||||
text: text,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [URL_VOLCENGINE_TRAN, init];
|
||||
};
|
||||
|
||||
const genOpenAI = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
// 兼容历史上作为systemPrompt的prompt,如果prompt中不包含带翻译文本,则添加文本到prompt末尾
|
||||
// if (!prompt.includes(INPUT_PLACE_TEXT)) {
|
||||
// prompt += `\nSource Text: ${INPUT_PLACE_TEXT}`;
|
||||
// }
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature,
|
||||
max_completion_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`, // OpenAI
|
||||
"api-key": key, // Azure OpenAI
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genGemini = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
url = url
|
||||
.replaceAll(INPUT_PLACE_MODEL, model)
|
||||
.replaceAll(INPUT_PLACE_KEY, key);
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
system_instruction: {
|
||||
parts: {
|
||||
text: systemPrompt,
|
||||
},
|
||||
},
|
||||
contents: {
|
||||
role: "user",
|
||||
parts: {
|
||||
text: userPrompt,
|
||||
},
|
||||
},
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature,
|
||||
// topP: 0.8,
|
||||
// topK: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genGemini2 = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genClaude = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"x-api-key": key,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genOllama = ({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
think,
|
||||
url,
|
||||
key,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
model,
|
||||
}) => {
|
||||
systemPrompt = systemPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
userPrompt = userPrompt
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||
|
||||
const data = {
|
||||
model,
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
think: think,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
||||
const data = {
|
||||
text,
|
||||
source_lang: from,
|
||||
target_lang: to,
|
||||
};
|
||||
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
const genCustom = ({ text, from, to, url, key, reqHook }) => {
|
||||
url = url
|
||||
.replaceAll(INPUT_PLACE_URL, url)
|
||||
.replaceAll(INPUT_PLACE_FROM, from)
|
||||
.replaceAll(INPUT_PLACE_TO, to)
|
||||
.replaceAll(INPUT_PLACE_TEXT, text)
|
||||
.replaceAll(INPUT_PLACE_KEY, key);
|
||||
let init = {};
|
||||
|
||||
if (reqHook?.trim()) {
|
||||
interpreter.run(`exports.reqHook = ${reqHook}`);
|
||||
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
|
||||
return [url, init];
|
||||
}
|
||||
|
||||
const data = {
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
if (key) {
|
||||
init.headers.Authorization = `Bearer ${key}`;
|
||||
}
|
||||
|
||||
return [url, init];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造翻译接口请求参数
|
||||
* @param {*}
|
||||
* @returns
|
||||
*/
|
||||
export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||
const args = { text, from, to, ...apiSetting };
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_DEEPL:
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
case OPT_TRANS_GEMINI:
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
case OPT_TRANS_CLAUDE:
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
case OPT_TRANS_NIUTRANS:
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
case OPT_TRANS_CUSTOMIZE_4:
|
||||
case OPT_TRANS_CUSTOMIZE_5:
|
||||
args.key = keyPick(translator, args.key, keyMap);
|
||||
break;
|
||||
case OPT_TRANS_DEEPLX:
|
||||
args.url = keyPick(translator, args.url, urlMap);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
return genGoogle(args);
|
||||
case OPT_TRANS_GOOGLE_2:
|
||||
return genGoogle2(args);
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
return genMicrosoft(args);
|
||||
case OPT_TRANS_DEEPL:
|
||||
return genDeepl(args);
|
||||
case OPT_TRANS_DEEPLFREE:
|
||||
return genDeeplFree(args);
|
||||
case OPT_TRANS_DEEPLX:
|
||||
return genDeeplX(args);
|
||||
case OPT_TRANS_NIUTRANS:
|
||||
return genNiuTrans(args);
|
||||
case OPT_TRANS_BAIDU:
|
||||
return genBaidu(args);
|
||||
case OPT_TRANS_TENCENT:
|
||||
return genTencent(args);
|
||||
case OPT_TRANS_VOLCENGINE:
|
||||
return genVolcengine(args);
|
||||
case OPT_TRANS_OPENAI:
|
||||
case OPT_TRANS_OPENAI_2:
|
||||
case OPT_TRANS_OPENAI_3:
|
||||
return genOpenAI(args);
|
||||
case OPT_TRANS_GEMINI:
|
||||
return genGemini(args);
|
||||
case OPT_TRANS_GEMINI_2:
|
||||
return genGemini2(args);
|
||||
case OPT_TRANS_CLAUDE:
|
||||
return genClaude(args);
|
||||
case OPT_TRANS_CLOUDFLAREAI:
|
||||
return genCloudflareAI(args);
|
||||
case OPT_TRANS_OLLAMA:
|
||||
case OPT_TRANS_OLLAMA_2:
|
||||
case OPT_TRANS_OLLAMA_3:
|
||||
return genOllama(args);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
case OPT_TRANS_CUSTOMIZE_2:
|
||||
case OPT_TRANS_CUSTOMIZE_3:
|
||||
case OPT_TRANS_CUSTOMIZE_4:
|
||||
case OPT_TRANS_CUSTOMIZE_5:
|
||||
return genCustom(args);
|
||||
default:
|
||||
throw new Error(`[trans] translator: ${translator} not support`);
|
||||
}
|
||||
};
|
||||
265
src/background.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import {
|
||||
MSG_FETCH,
|
||||
MSG_GET_HTTPCACHE,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_OPEN_OPTIONS,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_CONTEXT_MENUS,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
MSG_INJECT_JS,
|
||||
MSG_INJECT_CSS,
|
||||
MSG_UPDATE_CSP,
|
||||
DEFAULT_CSPLIST,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
CMD_OPEN_OPTIONS,
|
||||
CMD_OPEN_TRANBOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||
import { trySyncSettingAndRules } from "./libs/sync";
|
||||
import { fetchHandle, getHttpCache } from "./libs/fetch";
|
||||
import { sendTabMsg } from "./libs/msg";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { tryClearCaches } from "./libs";
|
||||
import { saveRule } from "./libs/rules";
|
||||
import { getCurTabId } from "./libs/msg";
|
||||
import { injectInlineJs, injectInternalCss } from "./libs/injector";
|
||||
import { kissLog } from "./libs/log";
|
||||
|
||||
globalThis.ContextType = "BACKGROUND";
|
||||
|
||||
const REMOVE_HEADERS = [
|
||||
`content-security-policy`,
|
||||
`content-security-policy-report-only`,
|
||||
`x-webkit-csp`,
|
||||
`x-content-security-policy`,
|
||||
];
|
||||
|
||||
/**
|
||||
* 添加右键菜单
|
||||
*/
|
||||
async function addContextMenus(contextMenuType = 1) {
|
||||
// 添加前先删除,避免重复ID的错误
|
||||
try {
|
||||
await browser.contextMenus.removeAll();
|
||||
} catch (err) {
|
||||
kissLog(err, "remove contextMenus");
|
||||
}
|
||||
|
||||
switch (contextMenuType) {
|
||||
case 1:
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_TRANSLATE,
|
||||
title: browser.i18n.getMessage("app_name"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_TRANSLATE,
|
||||
title: browser.i18n.getMessage("toggle_translate"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_TOGGLE_STYLE,
|
||||
title: browser.i18n.getMessage("toggle_style"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_OPEN_TRANBOX,
|
||||
title: browser.i18n.getMessage("open_tranbox"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: "options_separator",
|
||||
type: "separator",
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
browser.contextMenus.create({
|
||||
id: CMD_OPEN_OPTIONS,
|
||||
title: browser.i18n.getMessage("open_options"),
|
||||
contexts: ["page", "selection"],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新CSP策略
|
||||
* @param {*} csplist
|
||||
*/
|
||||
async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
|
||||
try {
|
||||
const newRules = csplist
|
||||
.split(/\n|,/)
|
||||
.map((url) => url.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, idx) => ({
|
||||
id: idx + 1,
|
||||
action: {
|
||||
type: "modifyHeaders",
|
||||
responseHeaders: REMOVE_HEADERS.map((header) => ({
|
||||
operation: "remove",
|
||||
header,
|
||||
})),
|
||||
},
|
||||
condition: {
|
||||
urlFilter: url,
|
||||
resourceTypes: ["main_frame", "sub_frame"],
|
||||
},
|
||||
}));
|
||||
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
|
||||
const oldRuleIds = oldRules.map((rule) => rule.id);
|
||||
await browser.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: oldRuleIds,
|
||||
addRules: newRules,
|
||||
});
|
||||
} catch (err) {
|
||||
kissLog(err, "update csp rules");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册邮件显示脚本
|
||||
*/
|
||||
async function registerMsgDisplayScript() {
|
||||
await messenger.messageDisplayScripts.register({
|
||||
js: [{ file: "/content.js" }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
tryInitDefaultData();
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
addContextMenus();
|
||||
|
||||
// 禁用CSP
|
||||
updateCspRules();
|
||||
});
|
||||
|
||||
/**
|
||||
* 浏览器启动
|
||||
*/
|
||||
browser.runtime.onStartup.addListener(async () => {
|
||||
// 同步数据
|
||||
await trySyncSettingAndRules();
|
||||
|
||||
const { clearCache, contextMenuType, subrulesList, csplist } =
|
||||
await getSettingWithDefault();
|
||||
|
||||
// 清除缓存
|
||||
if (clearCache) {
|
||||
tryClearCaches();
|
||||
}
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
// firefox重启后菜单会消失,故重复添加
|
||||
addContextMenus(contextMenuType);
|
||||
|
||||
// 禁用CSP
|
||||
updateCspRules(csplist);
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules({ subrulesList });
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听消息
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_FETCH:
|
||||
return await fetchHandle(args);
|
||||
case MSG_GET_HTTPCACHE:
|
||||
const { input, init } = args;
|
||||
return await getHttpCache(input, init);
|
||||
case MSG_OPEN_OPTIONS:
|
||||
return await browser.runtime.openOptionsPage();
|
||||
case MSG_SAVE_RULE:
|
||||
return await saveRule(args);
|
||||
case MSG_INJECT_JS:
|
||||
return await browser.scripting.executeScript({
|
||||
target: { tabId: await getCurTabId(), allFrames: true },
|
||||
func: injectInlineJs,
|
||||
args: [args],
|
||||
world: "MAIN",
|
||||
});
|
||||
case MSG_INJECT_CSS:
|
||||
return await browser.scripting.executeScript({
|
||||
target: { tabId: await getCurTabId(), allFrames: true },
|
||||
func: injectInternalCss,
|
||||
args: [args],
|
||||
world: "MAIN",
|
||||
});
|
||||
case MSG_UPDATE_CSP:
|
||||
return await updateCspRules(args);
|
||||
case MSG_CONTEXT_MENUS:
|
||||
return await addContextMenus(args);
|
||||
case MSG_COMMAND_SHORTCUTS:
|
||||
return await browser.commands.getAll();
|
||||
default:
|
||||
throw new Error(`message action is unavailable: ${action}`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听快捷键
|
||||
*/
|
||||
browser.commands.onCommand.addListener((command) => {
|
||||
// console.log(`Command: ${command}`);
|
||||
switch (command) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听右键菜单
|
||||
*/
|
||||
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
|
||||
switch (menuItemId) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
271
src/common.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import Action from "./views/Action";
|
||||
import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
APP_LCNAME,
|
||||
DEFAULT_TRANBOX_SETTING,
|
||||
} from "./config";
|
||||
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
||||
import Slection from "./views/Selection";
|
||||
import { touchTapListener } from "./libs/touch";
|
||||
import { debounce, genEventName } from "./libs/utils";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
import { browser } from "./libs/browser";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { isInBlacklist } from "./libs/blacklist";
|
||||
import inputTranslate from "./libs/inputTranslate";
|
||||
|
||||
/**
|
||||
* 油猴脚本设置页面
|
||||
*/
|
||||
function runSettingPage() {
|
||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||
unsafeWindow.GM = GM;
|
||||
unsafeWindow.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
};
|
||||
} else {
|
||||
const ping = genEventName();
|
||||
window.addEventListener(ping, handlePing);
|
||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||
const script = document.createElement("script");
|
||||
script.textContent = `(${injectScript})("${ping}")`;
|
||||
document.head.append(script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件监听后端事件
|
||||
* @param {*} translator
|
||||
*/
|
||||
function runtimeListener(translator) {
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args);
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { rule: translator.rule, setting: translator.setting };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe 页面执行
|
||||
* @param {*} translator
|
||||
*/
|
||||
function runIframe(translator) {
|
||||
window.addEventListener("message", (e) => {
|
||||
const { action, args } = e.data || {};
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator?.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator?.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args || {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬浮按钮
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
async function showFab(translator) {
|
||||
const fab = await getFabWithDefault();
|
||||
const $action = document.createElement("div");
|
||||
$action.setAttribute("id", APP_LCNAME);
|
||||
$action.style.fontSize = "0";
|
||||
$action.style.width = "0";
|
||||
$action.style.height = "0";
|
||||
document.body.parentElement.appendChild($action);
|
||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: APP_LCNAME,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Action translator={translator} fab={fab} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 划词翻译
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
function showTransbox(
|
||||
{
|
||||
contextMenuType,
|
||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
||||
transApis,
|
||||
darkMode,
|
||||
uiLang,
|
||||
langDetector,
|
||||
},
|
||||
{ transSelected }
|
||||
) {
|
||||
if (transSelected === "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
const $tranbox = document.createElement("div");
|
||||
$tranbox.setAttribute("id", "kiss-transbox");
|
||||
$tranbox.style.fontSize = "0";
|
||||
$tranbox.style.width = "0";
|
||||
$tranbox.style.height = "0";
|
||||
document.body.parentElement.appendChild($tranbox);
|
||||
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowRootElement.classList.add(`KT-transbox`);
|
||||
shadowRootElement.classList.add(`KT-transbox_${darkMode ? "dark" : "light"}`);
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: "kiss-transbox",
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
ReactDOM.createRoot(shadowRootElement).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<Slection
|
||||
contextMenuType={contextMenuType}
|
||||
tranboxSetting={tranboxSetting}
|
||||
transApis={transApis}
|
||||
uiLang={uiLang}
|
||||
langDetector={langDetector}
|
||||
/>
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息到页面顶部
|
||||
* @param {*} message
|
||||
*/
|
||||
function showErr(message) {
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${message}`;
|
||||
$err.style.cssText = "background:red; color:#fff;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听触屏操作
|
||||
* @param {*} translator
|
||||
* @returns
|
||||
*/
|
||||
function touchOperation(translator) {
|
||||
const { touchTranslate = 2 } = translator.setting;
|
||||
if (touchTranslate === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTap = debounce(() => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
});
|
||||
touchTapListener(handleTap, touchTranslate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
export async function run(isUserscript = false) {
|
||||
try {
|
||||
const href = document.location.href;
|
||||
|
||||
// 设置页面
|
||||
if (
|
||||
isUserscript &&
|
||||
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||
href.includes(process.env.REACT_APP_OPTIONSPAGE))
|
||||
) {
|
||||
runSettingPage();
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取设置信息
|
||||
const setting = await getSettingWithDefault();
|
||||
|
||||
// 黑名单
|
||||
if (isInBlacklist(href, setting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译网页
|
||||
const rule = await matchRule(href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
|
||||
// 适配iframe
|
||||
if (isIframe) {
|
||||
runIframe(translator);
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听消息
|
||||
!isUserscript && runtimeListener(translator);
|
||||
|
||||
// 输入框翻译
|
||||
inputTranslate(setting);
|
||||
|
||||
// 划词翻译
|
||||
showTransbox(setting, rule);
|
||||
|
||||
// 浮球按钮
|
||||
await showFab(translator);
|
||||
|
||||
// 触屏操作
|
||||
touchOperation(translator);
|
||||
|
||||
// 同步订阅规则
|
||||
isUserscript && (await trySyncAllSubRules(setting));
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
showErr(err.message);
|
||||
}
|
||||
}
|
||||
4
src/config/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
995
src/config/i18n.js
Normal file
@@ -0,0 +1,995 @@
|
||||
export const UI_LANGS = [
|
||||
["en", "English"],
|
||||
["zh", "中文"],
|
||||
];
|
||||
|
||||
const customApiLangs = `["en", "English - English"],
|
||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||
["ar", "Arabic - العربية"],
|
||||
["bg", "Bulgarian - Български"],
|
||||
["ca", "Catalan - Català"],
|
||||
["hr", "Croatian - Hrvatski"],
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
["el", "Greek - Ελληνικά"],
|
||||
["hi", "Hindi - हिन्दी"],
|
||||
["hu", "Hungarian - Magyar"],
|
||||
["id", "Indonesian - Indonesia"],
|
||||
["it", "Italian - Italiano"],
|
||||
["ja", "Japanese - 日本語"],
|
||||
["ko", "Korean - 한국어"],
|
||||
["ms", "Malay - Melayu"],
|
||||
["mt", "Maltese - Malti"],
|
||||
["nb", "Norwegian - Norsk Bokmål"],
|
||||
["pl", "Polish - Polski"],
|
||||
["pt", "Portuguese - Português"],
|
||||
["ro", "Romanian - Română"],
|
||||
["ru", "Russian - Русский"],
|
||||
["sk", "Slovak - Slovenčina"],
|
||||
["sl", "Slovenian - Slovenščina"],
|
||||
["es", "Spanish - Español"],
|
||||
["sv", "Swedish - Svenska"],
|
||||
["ta", "Tamil - தமிழ்"],
|
||||
["te", "Telugu - తెలుగు"],
|
||||
["th", "Thai - ไทย"],
|
||||
["tr", "Turkish - Türkçe"],
|
||||
["uk", "Ukrainian - Українська"],
|
||||
["vi", "Vietnamese - Tiếng Việt"],
|
||||
`;
|
||||
|
||||
const customApiHelpZH = `// 请求数据默认格式
|
||||
{
|
||||
"url": "{{url}}",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": "Bearer {{key}}"
|
||||
},
|
||||
"body": {
|
||||
"text": "{{text}}", // 待翻译文字
|
||||
"from": "{{from}}", // 文字的语言(可能为空)
|
||||
"to": "{{to}}", // 目标语言
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// 返回数据默认格式
|
||||
{
|
||||
text: "", // 翻译后的文字
|
||||
from: "", // 识别的源语言
|
||||
to: "", // 目标语言(可选)
|
||||
}
|
||||
|
||||
|
||||
// Hook 范例
|
||||
// URL
|
||||
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||
|
||||
// Request Hook
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
body: null,
|
||||
}]
|
||||
|
||||
// Response Hook
|
||||
// 其中返回数组第一个值表示译文字符串,第二个值为布尔值,表示原文语言与目标语言是否相同
|
||||
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||
|
||||
|
||||
// 支持的语言代码如下
|
||||
${customApiLangs}
|
||||
`;
|
||||
|
||||
const customApiHelpEN = `// Default request
|
||||
{
|
||||
"url": "{{url}}",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": "Bearer {{key}}"
|
||||
},
|
||||
"body": {
|
||||
"text": "{{text}}", // Text to be translated
|
||||
"from": "{{from}}", // The language of the text (may be empty)
|
||||
"to": "{{to}}", // Target language
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// Default response
|
||||
{
|
||||
text: "", // translated text
|
||||
from: "", // Recognized source language
|
||||
to: "", // Target language (optional)
|
||||
}
|
||||
|
||||
|
||||
/// Hook Example
|
||||
// URL
|
||||
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||
|
||||
// Request Hook
|
||||
(text, from, to, url, key) => [url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
body: null,
|
||||
}]
|
||||
|
||||
// Response Hook
|
||||
// In the returned array, the first value is the translated string, while the second value is a boolean
|
||||
// that indicates whether the source language is the same as the target language.
|
||||
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||
|
||||
|
||||
// The supported language codes are as follows
|
||||
${customApiLangs}
|
||||
`;
|
||||
|
||||
export const I18N = {
|
||||
app_name: {
|
||||
zh: `简约翻译`,
|
||||
en: `KISS Translator`,
|
||||
},
|
||||
translate: {
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
custom_api_help: {
|
||||
zh: customApiHelpZH,
|
||||
en: customApiHelpEN,
|
||||
},
|
||||
translate_alt: {
|
||||
zh: `翻译`,
|
||||
en: `Translate`,
|
||||
},
|
||||
basic_setting: {
|
||||
zh: `基本设置`,
|
||||
en: `Basic Setting`,
|
||||
},
|
||||
rules_setting: {
|
||||
zh: `规则设置`,
|
||||
en: `Rules Setting`,
|
||||
},
|
||||
apis_setting: {
|
||||
zh: `接口设置`,
|
||||
en: `Apis Setting`,
|
||||
},
|
||||
sync_setting: {
|
||||
zh: `同步设置`,
|
||||
en: `Sync Setting`,
|
||||
},
|
||||
patch_setting: {
|
||||
zh: `补丁设置`,
|
||||
en: `Patch Setting`,
|
||||
},
|
||||
patch_setting_help: {
|
||||
zh: `针对一些特殊网站的修正脚本,以便翻译软件得到更好的展示效果。`,
|
||||
en: `Corrected scripts for some special websites so that the translation software can get better display results.`,
|
||||
},
|
||||
inject_webfix: {
|
||||
zh: `注入修复补丁`,
|
||||
en: `Inject Webfix`,
|
||||
},
|
||||
about: {
|
||||
zh: `关于`,
|
||||
en: `About`,
|
||||
},
|
||||
about_md: {
|
||||
zh: `README.md`,
|
||||
en: `README.en.md`,
|
||||
},
|
||||
about_md_local: {
|
||||
zh: `请 [点击这里](${process.env.REACT_APP_HOMEPAGE}) 查看详情。`,
|
||||
en: `Please [click here](${process.env.REACT_APP_HOMEPAGE}) for details.`,
|
||||
},
|
||||
ui_lang: {
|
||||
zh: `界面语言`,
|
||||
en: `Interface Language`,
|
||||
},
|
||||
fetch_limit: {
|
||||
zh: `最大并发请求数量 (1-100)`,
|
||||
en: `Maximum Number Of Concurrent Requests (1-100)`,
|
||||
},
|
||||
if_think: {
|
||||
zh: `启用或禁用模型的深度思考能力`,
|
||||
en: `Enable or disable the model’s thinking behavior `,
|
||||
},
|
||||
think: {
|
||||
zh: `启用深度思考`,
|
||||
en: `enable thinking`,
|
||||
},
|
||||
nothink: {
|
||||
zh: `禁用深度思考`,
|
||||
en: `disable thinking`,
|
||||
},
|
||||
think_ignore: {
|
||||
zh: `忽略以下模型的<think>输出,逗号(,)分割,当模型支持思考但ollama不支持时需要填写本参数`,
|
||||
en: `Ignore the <think> block for the following models, comma (,) separated`,
|
||||
},
|
||||
fetch_interval: {
|
||||
zh: `每次请求间隔时间 (0-5000ms)`,
|
||||
en: `Time Between Requests (0-5000ms)`,
|
||||
},
|
||||
translate_interval: {
|
||||
zh: `重新翻译间隔时间 (100-5000ms)`,
|
||||
en: `Retranslation Interval (100-5000ms)`,
|
||||
},
|
||||
http_timeout: {
|
||||
zh: `请求超时时间 (5000-30000ms)`,
|
||||
en: `Request Timeout Time (5000-30000ms)`,
|
||||
},
|
||||
min_translate_length: {
|
||||
zh: `最小翻译字符数 (1-100)`,
|
||||
en: `Minimum number Of Translated Characters (1-100)`,
|
||||
},
|
||||
max_translate_length: {
|
||||
zh: `最大翻译字符数 (100-10000)`,
|
||||
en: `Maximum number Of Translated Characters (100-10000)`,
|
||||
},
|
||||
num_of_newline_characters: {
|
||||
zh: `换行字符数 (1-1000)`,
|
||||
en: `Number of Newline Characters (1-1000)`,
|
||||
},
|
||||
translate_service: {
|
||||
zh: `翻译服务`,
|
||||
en: `Translate Service`,
|
||||
},
|
||||
translate_timing: {
|
||||
zh: `翻译时机`,
|
||||
en: `Translate Timing`,
|
||||
},
|
||||
mk_pagescroll: {
|
||||
zh: `滚动加载翻译(推荐)`,
|
||||
en: `Rolling Loading (Suggested)`,
|
||||
},
|
||||
mk_pageopen: {
|
||||
zh: `页面打开全部翻译`,
|
||||
en: `Page Open`,
|
||||
},
|
||||
mk_mouseover: {
|
||||
zh: `鼠标悬停翻译`,
|
||||
en: `Mouseover`,
|
||||
},
|
||||
mk_ctrlKey: {
|
||||
zh: `Control + 鼠标悬停`,
|
||||
en: `Control + Mouseover`,
|
||||
},
|
||||
mk_shiftKey: {
|
||||
zh: `Shift + 鼠标悬停`,
|
||||
en: `Shift + Mouseover`,
|
||||
},
|
||||
mk_altKey: {
|
||||
zh: `Alt + 鼠标悬停`,
|
||||
en: `Alt + Mouseover`,
|
||||
},
|
||||
from_lang: {
|
||||
zh: `原文语言`,
|
||||
en: `Source Language`,
|
||||
},
|
||||
to_lang: {
|
||||
zh: `目标语言`,
|
||||
en: `Target Language`,
|
||||
},
|
||||
to_lang2: {
|
||||
zh: `第二目标语言`,
|
||||
en: `Target Language 2`,
|
||||
},
|
||||
to_lang2_helper: {
|
||||
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
|
||||
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
|
||||
},
|
||||
text_style: {
|
||||
zh: `译文样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
text_style_alt: {
|
||||
zh: `译文样式`,
|
||||
en: `Text Style`,
|
||||
},
|
||||
bg_color: {
|
||||
zh: `样式颜色`,
|
||||
en: `Style Color`,
|
||||
},
|
||||
remain_unchanged: {
|
||||
zh: `保留不变`,
|
||||
en: `Remain Unchanged`,
|
||||
},
|
||||
google_api: {
|
||||
zh: `谷歌翻译接口`,
|
||||
en: `Google Translate API`,
|
||||
},
|
||||
default_selector: {
|
||||
zh: `默认选择器`,
|
||||
en: `Default selector`,
|
||||
},
|
||||
selector_rules: {
|
||||
zh: `选择器规则`,
|
||||
en: `Selector Rules`,
|
||||
},
|
||||
save: {
|
||||
zh: `保存`,
|
||||
en: `Save`,
|
||||
},
|
||||
edit: {
|
||||
zh: `编辑`,
|
||||
en: `Edit`,
|
||||
},
|
||||
cancel: {
|
||||
zh: `取消`,
|
||||
en: `Cancel`,
|
||||
},
|
||||
delete: {
|
||||
zh: `删除`,
|
||||
en: `Delete`,
|
||||
},
|
||||
reset: {
|
||||
zh: `重置`,
|
||||
en: `Reset`,
|
||||
},
|
||||
add: {
|
||||
zh: `添加`,
|
||||
en: `Add`,
|
||||
},
|
||||
inject_rules: {
|
||||
zh: `注入订阅规则`,
|
||||
en: `Inject Subscribe Rules`,
|
||||
},
|
||||
personal_rules: {
|
||||
zh: `个人规则`,
|
||||
en: `Rules`,
|
||||
},
|
||||
subscribe_rules: {
|
||||
zh: `订阅规则`,
|
||||
en: `Subscribe`,
|
||||
},
|
||||
overwrite_subscribe_rules: {
|
||||
zh: `覆写订阅规则`,
|
||||
en: `Overwrite`,
|
||||
},
|
||||
subscribe_url: {
|
||||
zh: `订阅地址`,
|
||||
en: `Subscribe URL`,
|
||||
},
|
||||
rules_warn_1: {
|
||||
zh: `1、“个人规则”一直生效,选择“注入订阅规则”后,“订阅规则”才会生效。`,
|
||||
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
|
||||
},
|
||||
rules_warn_2: {
|
||||
zh: `2、“订阅规则”的注入位置是倒数第二的位置,因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
|
||||
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
|
||||
},
|
||||
rules_warn_3: {
|
||||
zh: `3、关于规则填写:输入框留空或下拉框选“*”表示采用全局规则。`,
|
||||
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
|
||||
},
|
||||
sync_warn: {
|
||||
zh: `涉及隐私数据的同步请谨慎选择第三方同步服务,建议自行搭建 kiss-worker 或 WebDAV 服务。`,
|
||||
en: `When synchronizing data that involves privacy, please be cautious about choosing third-party sync services. It is recommended to set up your own sync service using kiss-worker or WebDAV.`,
|
||||
},
|
||||
sync_warn_2: {
|
||||
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
|
||||
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
|
||||
},
|
||||
about_sync_api: {
|
||||
zh: `自建kiss-wroker数据同步服务`,
|
||||
en: `Self-hosting a Kiss-worker data sync service`,
|
||||
},
|
||||
about_api: {
|
||||
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
|
||||
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||
},
|
||||
about_api_proxy: {
|
||||
zh: `查看自建一个翻译接口代理`,
|
||||
en: `Check out the self-built translation interface proxy`,
|
||||
},
|
||||
style_none: {
|
||||
zh: `无`,
|
||||
en: `None`,
|
||||
},
|
||||
under_line: {
|
||||
zh: `下划直线`,
|
||||
en: `Underline`,
|
||||
},
|
||||
dot_line: {
|
||||
zh: `下划点状线`,
|
||||
en: `Dotted Underline`,
|
||||
},
|
||||
dash_line: {
|
||||
zh: `下划虚线`,
|
||||
en: `Dashed Underline`,
|
||||
},
|
||||
wavy_line: {
|
||||
zh: `下划波浪线`,
|
||||
en: `Wavy Underline`,
|
||||
},
|
||||
fuzzy: {
|
||||
zh: `模糊`,
|
||||
en: `Fuzzy`,
|
||||
},
|
||||
highlight: {
|
||||
zh: `高亮`,
|
||||
en: `Highlight`,
|
||||
},
|
||||
blockquote: {
|
||||
zh: `引用`,
|
||||
en: `Blockquote`,
|
||||
},
|
||||
diy_style: {
|
||||
zh: `自定义样式`,
|
||||
en: `Custom Style`,
|
||||
},
|
||||
diy_style_helper: {
|
||||
zh: `遵循“CSS”的语法`,
|
||||
en: `Follow the syntax of "CSS"`,
|
||||
},
|
||||
setting: {
|
||||
zh: `设置`,
|
||||
en: `Setting`,
|
||||
},
|
||||
pattern: {
|
||||
zh: `匹配网址`,
|
||||
en: `URL pattern`,
|
||||
},
|
||||
pattern_helper: {
|
||||
zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
|
||||
en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
|
||||
},
|
||||
selector_helper: {
|
||||
zh: `1、遵循CSS选择器语法。2、多个CSS选择器之间用“;”隔开。3、“shadow root”选择器和内部选择器用“>>>”隔开。`,
|
||||
en: `1. Follow CSS selector syntax. 2. Separate multiple CSS selectors with ";". 3. The "shadow root" selector and the internal selector are separated by ">>>".`,
|
||||
},
|
||||
translate_switch: {
|
||||
zh: `开启翻译`,
|
||||
en: `Translate Switch`,
|
||||
},
|
||||
default_enabled: {
|
||||
zh: `默认开启`,
|
||||
en: `Enabled`,
|
||||
},
|
||||
default_disabled: {
|
||||
zh: `默认关闭`,
|
||||
en: `Disabled`,
|
||||
},
|
||||
selector: {
|
||||
zh: `选择器`,
|
||||
en: `Selector`,
|
||||
},
|
||||
keep_selector: {
|
||||
zh: `保留元素选择器`,
|
||||
en: `Keep unchanged selector`,
|
||||
},
|
||||
keep_selector_helper: {
|
||||
zh: `1、遵循CSS选择器语法。`,
|
||||
en: `1. Follow CSS selector syntax.`,
|
||||
},
|
||||
terms: {
|
||||
zh: `专业术语`,
|
||||
en: `Terms`,
|
||||
},
|
||||
terms_helper: {
|
||||
zh: `1、支持正则表达式匹配,无需斜杆,不支持修饰符。2、多条术语用换行或分号“;”隔开。3、术语和译文用英文逗号“,”隔开。4、没有译文视为不翻译术语。`,
|
||||
en: `1. Supports regular expression matching, no slash required, and no modifiers are supported. 2. Separate multiple terms with newlines or semicolons ";". 3. Terms and translations are separated by English commas ",". 4. If there is no translation, the term will be deemed not to be translated.`,
|
||||
},
|
||||
selector_style: {
|
||||
zh: `选择器节点样式`,
|
||||
en: `Selector Style`,
|
||||
},
|
||||
selector_style_helper: {
|
||||
zh: `开启翻译时注入,关闭翻译时不会移除。`,
|
||||
en: `It is injected when translation is turned on and will not be removed when translation is turned off.`,
|
||||
},
|
||||
selector_parent_style: {
|
||||
zh: `选择器父节点样式`,
|
||||
en: `Selector Parent Style`,
|
||||
},
|
||||
inject_js: {
|
||||
zh: `注入JS`,
|
||||
en: `Inject JS`,
|
||||
},
|
||||
inject_js_helper: {
|
||||
zh: `1、开启翻译时注入运行,关闭翻译时移除。2、随着页面变化,可能会多次注入运行。`,
|
||||
en: `1. Inject and run when translation is turned on, and removed when translation is turned off. 2. As the page changes, it may be injected and run multiple times.`,
|
||||
},
|
||||
inject_css: {
|
||||
zh: `注入CSS`,
|
||||
en: `Inject CSS`,
|
||||
},
|
||||
inject_css_helper: {
|
||||
zh: `开启翻译时注入,关闭翻译时将移除。`,
|
||||
en: `Injected when translation is enabled and removed when translation is disabled.`,
|
||||
},
|
||||
root_selector: {
|
||||
zh: `根选择器`,
|
||||
en: `Root Selector`,
|
||||
},
|
||||
fixer_function: {
|
||||
zh: `修复函数`,
|
||||
en: `Fixer Function`,
|
||||
},
|
||||
fixer_function_helper: {
|
||||
zh: `1、br是将<br>换行替换成<p "kiss-p">。2、bn是将\\n换行替换成<p "kiss-p">。3、brToDiv和bnToDiv是替换成<div class="kiss-p">。`,
|
||||
en: `1. br replaces <br> line breaks with <p "kiss-p">. 2. bn replaces \\n newline with <p "kiss-p">. 3. brToDiv and bnToDiv are replaced with <div class="kiss-p">.`,
|
||||
},
|
||||
import: {
|
||||
zh: `导入`,
|
||||
en: `Import`,
|
||||
},
|
||||
export: {
|
||||
zh: `导出`,
|
||||
en: `Export`,
|
||||
},
|
||||
export_translation: {
|
||||
zh: `导出释义`,
|
||||
en: `Export Translation`,
|
||||
},
|
||||
error_cant_be_blank: {
|
||||
zh: `不能为空`,
|
||||
en: `Can not be blank`,
|
||||
},
|
||||
error_duplicate_values: {
|
||||
zh: `存在重复的值`,
|
||||
en: `There are duplicate values`,
|
||||
},
|
||||
error_wrong_file_type: {
|
||||
zh: `错误的文件类型`,
|
||||
en: `Wrong file type`,
|
||||
},
|
||||
error_fetch_url: {
|
||||
zh: `请检查url地址是否正确或稍后再试。`,
|
||||
en: `Please check if the url address is correct or try again later.`,
|
||||
},
|
||||
deepl_api: {
|
||||
zh: `DeepL 接口`,
|
||||
en: `DeepL API`,
|
||||
},
|
||||
deepl_key: {
|
||||
zh: `DeepL 密钥`,
|
||||
en: `DeepL Key`,
|
||||
},
|
||||
openai_api: {
|
||||
zh: `OpenAI 接口`,
|
||||
en: `OpenAI API`,
|
||||
},
|
||||
openai_key: {
|
||||
zh: `OpenAI 密钥`,
|
||||
en: `OpenAI Key`,
|
||||
},
|
||||
openai_model: {
|
||||
zh: `OpenAI 模型`,
|
||||
en: `OpenAI Model`,
|
||||
},
|
||||
openai_prompt: {
|
||||
zh: `OpenAI 提示词`,
|
||||
en: `OpenAI Prompt`,
|
||||
},
|
||||
if_clear_cache: {
|
||||
zh: `是否清除缓存`,
|
||||
en: `Whether clear cache`,
|
||||
},
|
||||
clear_cache_never: {
|
||||
zh: `不清除缓存`,
|
||||
en: `Never clear cache`,
|
||||
},
|
||||
clear_cache_restart: {
|
||||
zh: `重启浏览器时清除缓存`,
|
||||
en: `Clear cache when restarting browser`,
|
||||
},
|
||||
data_sync_type: {
|
||||
zh: `数据同步方式`,
|
||||
en: `Data Sync Type`,
|
||||
},
|
||||
data_sync_url: {
|
||||
zh: `数据同步接口`,
|
||||
en: `Data Sync API`,
|
||||
},
|
||||
data_sync_user: {
|
||||
zh: `数据同步账户`,
|
||||
en: `Data Sync User`,
|
||||
},
|
||||
data_sync_key: {
|
||||
zh: `数据同步密钥`,
|
||||
en: `Data Sync Key`,
|
||||
},
|
||||
sync_now: {
|
||||
zh: `立即同步`,
|
||||
en: `Sync Now`,
|
||||
},
|
||||
sync_success: {
|
||||
zh: `同步成功!`,
|
||||
en: `Sync Success`,
|
||||
},
|
||||
sync_failed: {
|
||||
zh: `同步失败!`,
|
||||
en: `Sync Error`,
|
||||
},
|
||||
error_got_some_wrong: {
|
||||
zh: `抱歉,出错了!`,
|
||||
en: `Sorry, something went wrong!`,
|
||||
},
|
||||
error_sync_setting: {
|
||||
zh: `您的同步类型必须为“KISS-Worker”,且需填写完整`,
|
||||
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
|
||||
},
|
||||
click_test: {
|
||||
zh: `点击测试`,
|
||||
en: `Click Test`,
|
||||
},
|
||||
test_success: {
|
||||
zh: `测试成功`,
|
||||
en: `Test success`,
|
||||
},
|
||||
test_failed: {
|
||||
zh: `测试失败`,
|
||||
en: `Test failed`,
|
||||
},
|
||||
clear_all_cache_now: {
|
||||
zh: `立即清除全部缓存`,
|
||||
en: `Clear all cache now`,
|
||||
},
|
||||
clear_cache: {
|
||||
zh: `清除缓存`,
|
||||
en: `Clear Cache`,
|
||||
},
|
||||
clear_success: {
|
||||
zh: `清除成功`,
|
||||
en: `Clear success`,
|
||||
},
|
||||
clear_failed: {
|
||||
zh: `清除失败`,
|
||||
en: `Clear failed`,
|
||||
},
|
||||
share: {
|
||||
zh: `分享`,
|
||||
en: `Share`,
|
||||
},
|
||||
clear_all: {
|
||||
zh: `清空`,
|
||||
en: `Clear All`,
|
||||
},
|
||||
help: {
|
||||
zh: `求助`,
|
||||
en: `Help`,
|
||||
},
|
||||
restore_default: {
|
||||
zh: `恢复默认`,
|
||||
en: `Restore Default`,
|
||||
},
|
||||
shortcuts_setting: {
|
||||
zh: `快捷键设置`,
|
||||
en: `Shortcuts Setting`,
|
||||
},
|
||||
toggle_translate_shortcut: {
|
||||
zh: `"开启翻译"快捷键`,
|
||||
en: `"Toggle Translate" Shortcut`,
|
||||
},
|
||||
toggle_style_shortcut: {
|
||||
zh: `"切换样式"快捷键`,
|
||||
en: `"Toggle Style" Shortcut`,
|
||||
},
|
||||
toggle_popup_shortcut: {
|
||||
zh: `"打开弹窗"快捷键`,
|
||||
en: `"Open Popup" Shortcut`,
|
||||
},
|
||||
open_setting_shortcut: {
|
||||
zh: `"打开设置"快捷键`,
|
||||
en: `"Open Setting" Shortcut`,
|
||||
},
|
||||
hide_fab_button: {
|
||||
zh: `隐藏悬浮按钮`,
|
||||
en: `Hide Fab Button`,
|
||||
},
|
||||
hide_tran_button: {
|
||||
zh: `隐藏翻译按钮`,
|
||||
en: `Hide Translate Button`,
|
||||
},
|
||||
hide_click_away: {
|
||||
zh: `点击外部关闭弹窗`,
|
||||
en: `Click outside to close the pop-up window`,
|
||||
},
|
||||
use_simple_style: {
|
||||
zh: `使用简洁界面`,
|
||||
en: `Use a simple interface`,
|
||||
},
|
||||
show: {
|
||||
zh: `显示`,
|
||||
en: `Show`,
|
||||
},
|
||||
hide: {
|
||||
zh: `隐藏`,
|
||||
en: `Hide`,
|
||||
},
|
||||
save_rule: {
|
||||
zh: `保存规则`,
|
||||
en: `Save Rule`,
|
||||
},
|
||||
global_rule: {
|
||||
zh: `全局规则`,
|
||||
en: `Global Rule`,
|
||||
},
|
||||
input_translate: {
|
||||
zh: `输入框翻译`,
|
||||
en: `Input Box Translation`,
|
||||
},
|
||||
use_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: `默认为单击“AltLeft+KeyI”`,
|
||||
en: `Default is "AltLeft+KeyI"`,
|
||||
},
|
||||
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"`,
|
||||
},
|
||||
detect_lang_remote: {
|
||||
zh: `远程语言检测`,
|
||||
en: `Remote language detection`,
|
||||
},
|
||||
detect_lang_remote_help: {
|
||||
zh: `启用后检测准确度增加,但会降低翻译速度,请酌情开启`,
|
||||
en: `After enabling, the detection accuracy will increase, but it will reduce the translation speed. Please enable it as appropriate.`,
|
||||
},
|
||||
disable: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
},
|
||||
enable: {
|
||||
zh: `启用`,
|
||||
en: `Enable`,
|
||||
},
|
||||
selection_translate: {
|
||||
zh: `划词翻译`,
|
||||
en: `Selection Translate`,
|
||||
},
|
||||
toggle_selection_translate: {
|
||||
zh: `启用划词翻译`,
|
||||
en: `Use Selection Translate`,
|
||||
},
|
||||
trigger_tranbox_shortcut: {
|
||||
zh: `显示翻译框/翻译选中文字快捷键`,
|
||||
en: `Open Translate Popup/Translate Selected Shortcut`,
|
||||
},
|
||||
tranbtn_offset_x: {
|
||||
zh: `翻译按钮偏移X(±200)`,
|
||||
en: `Translate Button Offset X (±200)`,
|
||||
},
|
||||
tranbtn_offset_y: {
|
||||
zh: `翻译按钮偏移Y(±200)`,
|
||||
en: `Translate Button Offset Y (±200)`,
|
||||
},
|
||||
tranbox_offset_x: {
|
||||
zh: `翻译框偏移X(±200)`,
|
||||
en: `Translate Box Offset X (±200)`,
|
||||
},
|
||||
tranbox_offset_y: {
|
||||
zh: `翻译框偏移Y(±200)`,
|
||||
en: `Translate Box Offset Y (±200)`,
|
||||
},
|
||||
translated_text: {
|
||||
zh: `译文`,
|
||||
en: `Translated Text`,
|
||||
},
|
||||
original_text: {
|
||||
zh: `原文`,
|
||||
en: `Original Text`,
|
||||
},
|
||||
favorite_words: {
|
||||
zh: `收藏词汇`,
|
||||
en: `Favorite Words`,
|
||||
},
|
||||
touch_setting: {
|
||||
zh: `触屏设置`,
|
||||
en: `Touch Setting`,
|
||||
},
|
||||
touch_translate_shortcut: {
|
||||
zh: `触屏翻译快捷方式`,
|
||||
en: `Touch Translate Shortcut`,
|
||||
},
|
||||
touch_tap_0: {
|
||||
zh: `禁用`,
|
||||
en: `Disable`,
|
||||
},
|
||||
touch_tap_2: {
|
||||
zh: `双指轻触`,
|
||||
en: `Two finger tap`,
|
||||
},
|
||||
touch_tap_3: {
|
||||
zh: `三指轻触`,
|
||||
en: `Three finger tap`,
|
||||
},
|
||||
touch_tap_4: {
|
||||
zh: `四指轻触`,
|
||||
en: `Four finger tap`,
|
||||
},
|
||||
translate_blacklist: {
|
||||
zh: `禁用翻译名单`,
|
||||
en: `Translate Blacklist`,
|
||||
},
|
||||
disabled_csplist: {
|
||||
zh: `禁用CSP名单`,
|
||||
en: `Disabled CSP List`,
|
||||
},
|
||||
disabled_csplist_helper: {
|
||||
zh: `3、通过调整CSP策略,使得某些页面能够注入JS/CSS/Media,请谨慎使用,除非您已知晓相关风险。`,
|
||||
en: `3. By adjusting the CSP policy, some pages can inject JS/CSS/Media. Please use it with caution unless you are aware of the related risks.`,
|
||||
},
|
||||
skip_langs: {
|
||||
zh: `不翻译的语言`,
|
||||
en: `Disable Languages`,
|
||||
},
|
||||
skip_langs_helper: {
|
||||
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
|
||||
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
|
||||
},
|
||||
context_menus: {
|
||||
zh: `右键菜单`,
|
||||
en: `Context Menus`,
|
||||
},
|
||||
hide_context_menus: {
|
||||
zh: `隐藏右键菜单`,
|
||||
en: `Hide Context Menus`,
|
||||
},
|
||||
simple_context_menus: {
|
||||
zh: `简单右键菜单`,
|
||||
en: `Simple_context_menus Context Menus`,
|
||||
},
|
||||
secondary_context_menus: {
|
||||
zh: `二级右键菜单`,
|
||||
en: `Secondary Context Menus`,
|
||||
},
|
||||
mulkeys_help: {
|
||||
zh: `支持用换行或英文逗号“,”分隔,轮询调用。`,
|
||||
en: `Supports polling calls separated by newlines or English commas ",".`,
|
||||
},
|
||||
translation_element_tag: {
|
||||
zh: `译文元素标签`,
|
||||
en: `Translation Element Tag`,
|
||||
},
|
||||
show_only_translations: {
|
||||
zh: `仅显示译文`,
|
||||
en: `Show Only Translations`,
|
||||
},
|
||||
show_only_translations_help: {
|
||||
zh: `非完美实现,某些页面可能有样式等问题。`,
|
||||
en: `It is not a perfect implementation and some pages may have style issues.`,
|
||||
},
|
||||
translate_page_title: {
|
||||
zh: `是否翻译页面标题`,
|
||||
en: `Translate Page Title`,
|
||||
},
|
||||
more: {
|
||||
zh: `更多`,
|
||||
en: `More`,
|
||||
},
|
||||
less: {
|
||||
zh: `更少`,
|
||||
en: `Less`,
|
||||
},
|
||||
fixer_selector: {
|
||||
zh: `网页修复选择器`,
|
||||
en: `Fixer Selector`,
|
||||
},
|
||||
reg_niutrans: {
|
||||
zh: `获取小牛翻译密钥【简约翻译专属新用户注册赠送300万字符】`,
|
||||
en: `Get NiuTrans APIKey [KISS Translator Exclusive New User Registration Free 3 Million Characters]`,
|
||||
},
|
||||
trigger_mode: {
|
||||
zh: `触发方式`,
|
||||
en: `Trigger Mode`,
|
||||
},
|
||||
trigger_click: {
|
||||
zh: `点击触发`,
|
||||
en: `Click Trigger`,
|
||||
},
|
||||
trigger_hover: {
|
||||
zh: `鼠标悬停触发`,
|
||||
en: `Hover Trigger`,
|
||||
},
|
||||
trigger_select: {
|
||||
zh: `选中触发`,
|
||||
en: `Select Trigger`,
|
||||
},
|
||||
extend_styles: {
|
||||
zh: `附加样式`,
|
||||
en: `Extend Styles`,
|
||||
},
|
||||
custom_option: {
|
||||
zh: `自定义选项`,
|
||||
en: `Custom Option`,
|
||||
},
|
||||
translate_selected_text: {
|
||||
zh: `翻译选中文字`,
|
||||
en: `Translate Selected Text`,
|
||||
},
|
||||
toggle_style: {
|
||||
zh: `切换样式`,
|
||||
en: `Toggle Style`,
|
||||
},
|
||||
open_menu: {
|
||||
zh: `打开弹窗菜单`,
|
||||
en: `Open Popup Menu`,
|
||||
},
|
||||
open_setting: {
|
||||
zh: `打开设置`,
|
||||
en: `Open Setting`,
|
||||
},
|
||||
follow_selection: {
|
||||
zh: `翻译框跟随选中文本`,
|
||||
en: `Transbox Follow Selection`,
|
||||
},
|
||||
translate_start_hook: {
|
||||
zh: `翻译开始钩子函数`,
|
||||
en: `Translate Start Hook`,
|
||||
},
|
||||
translate_start_hook_helper: {
|
||||
zh: `翻译开始时运行,入参为: 翻译节点,原文文本。`,
|
||||
en: `Run when translation starts, the input parameters are: translation node, original text.`,
|
||||
},
|
||||
translate_end_hook: {
|
||||
zh: `翻译完成钩子函数`,
|
||||
en: `Translate End Hook`,
|
||||
},
|
||||
translate_end_hook_helper: {
|
||||
zh: `翻译完成时运行,入参为: 翻译节点,原文文本,译文文本,保留元素。`,
|
||||
en: `Run when the translation is completed, the input parameters are: translation node, original text, translation text, retained elements.`,
|
||||
},
|
||||
translate_remove_hook: {
|
||||
zh: `翻译移除钩子函数`,
|
||||
en: `Translate Removed Hook`,
|
||||
},
|
||||
translate_remove_hook_helper: {
|
||||
zh: `翻译移除时运行,入参为: 翻译节点。`,
|
||||
en: `Run when translation is removed, the input parameters are: translation node.`,
|
||||
},
|
||||
english_dict: {
|
||||
zh: `英文词典`,
|
||||
en: `English Dictionary`,
|
||||
},
|
||||
api_name: {
|
||||
zh: `接口名称`,
|
||||
en: `API Name`,
|
||||
},
|
||||
is_disabled: {
|
||||
zh: `是否禁用`,
|
||||
en: `Is Disabled`,
|
||||
},
|
||||
translate_selected: {
|
||||
zh: `是否启用划词翻译`,
|
||||
en: `If translate selected`,
|
||||
},
|
||||
};
|
||||
806
src/config/index.js
Normal file
@@ -0,0 +1,806 @@
|
||||
import {
|
||||
DEFAULT_SELECTOR,
|
||||
DEFAULT_KEEP_SELECTOR,
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
} from "./rules";
|
||||
import { APP_NAME, APP_LCNAME } from "./app";
|
||||
export { I18N, UI_LANGS } from "./i18n";
|
||||
export {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
APP_LCNAME,
|
||||
};
|
||||
|
||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
|
||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||
export const CMD_OPEN_OPTIONS = "openOptions";
|
||||
export const CMD_OPEN_TRANBOX = "openTranbox";
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
export const CLIENT_EDGE = "edge";
|
||||
export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_THUNDERBIRD = "thunderbird";
|
||||
export const CLIENT_EXTS = [
|
||||
CLIENT_CHROME,
|
||||
CLIENT_EDGE,
|
||||
CLIENT_FIREFOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
];
|
||||
|
||||
export const KV_RULES_KEY = "kiss-rules.json";
|
||||
export const KV_WORDS_KEY = "kiss-words.json";
|
||||
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
|
||||
export const KV_SETTING_KEY = "kiss-setting.json";
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
|
||||
export const MSG_FETCH = "fetch";
|
||||
export const MSG_GET_HTTPCACHE = "get_httpcache";
|
||||
export const MSG_OPEN_OPTIONS = "open_options";
|
||||
export const MSG_SAVE_RULE = "save_rule";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_OPEN_TRANBOX = "open_tranbox";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
|
||||
export const MSG_INJECT_JS = "inject_js";
|
||||
export const MSG_INJECT_CSS = "inject_css";
|
||||
export const MSG_UPDATE_CSP = "update_csp";
|
||||
|
||||
export const THEME_LIGHT = "light";
|
||||
export const THEME_DARK = "dark";
|
||||
|
||||
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
|
||||
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
|
||||
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
|
||||
export const URL_KISS_RULES_NEW_ISSUE =
|
||||
"https://github.com/fishjar/kiss-rules/issues/new";
|
||||
export const URL_RAW_PREFIX =
|
||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||
|
||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||
|
||||
// api.cognitive.microsofttranslator.com
|
||||
export const URL_MICROSOFT_TRAN =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/translate";
|
||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
||||
export const URL_MICROSOFT_LANGDETECT =
|
||||
"https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
|
||||
|
||||
export const URL_GOOGLE_TRAN =
|
||||
"https://translate.googleapis.com/translate_a/single";
|
||||
export const URL_GOOGLE_TRAN2 =
|
||||
"https://translate-pa.googleapis.com/v1/translateHtml";
|
||||
export const DEFAULT_GOOGLE_API_KEY = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520";
|
||||
|
||||
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
|
||||
export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug";
|
||||
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
|
||||
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
|
||||
export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
|
||||
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
|
||||
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
|
||||
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
|
||||
export const URL_VOLCENGINE_TRAN =
|
||||
"https://translate.volcengine.com/crx/translate/v1";
|
||||
export const URL_NIUTRANS_REG =
|
||||
"https://niutrans.com/login?active=3&userSource=kiss-translator";
|
||||
|
||||
export const DEFAULT_USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||
|
||||
export const OPT_DICT_BAIDU = "Baidu";
|
||||
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_DEEPL = "DeepL";
|
||||
export const OPT_TRANS_DEEPLX = "DeepLX";
|
||||
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
||||
export const OPT_TRANS_NIUTRANS = "NiuTrans";
|
||||
export const OPT_TRANS_BAIDU = "Baidu";
|
||||
export const OPT_TRANS_TENCENT = "Tencent";
|
||||
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_OPENAI_2 = "OpenAI2";
|
||||
export const OPT_TRANS_OPENAI_3 = "OpenAI3";
|
||||
export const OPT_TRANS_GEMINI = "Gemini";
|
||||
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||
export const OPT_TRANS_CLAUDE = "Claude";
|
||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||
export const OPT_TRANS_OLLAMA_2 = "Ollama2";
|
||||
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
|
||||
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
|
||||
export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
|
||||
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
|
||||
export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
];
|
||||
|
||||
export const OPT_LANGDETECTOR_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
["en", "English - English"],
|
||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||
["ar", "Arabic - العربية"],
|
||||
["bg", "Bulgarian - Български"],
|
||||
["ca", "Catalan - Català"],
|
||||
["hr", "Croatian - Hrvatski"],
|
||||
["cs", "Czech - Čeština"],
|
||||
["da", "Danish - Dansk"],
|
||||
["nl", "Dutch - Nederlands"],
|
||||
["fi", "Finnish - Suomi"],
|
||||
["fr", "French - Français"],
|
||||
["de", "German - Deutsch"],
|
||||
["el", "Greek - Ελληνικά"],
|
||||
["hi", "Hindi - हिन्दी"],
|
||||
["hu", "Hungarian - Magyar"],
|
||||
["id", "Indonesian - Indonesia"],
|
||||
["it", "Italian - Italiano"],
|
||||
["ja", "Japanese - 日本語"],
|
||||
["ko", "Korean - 한국어"],
|
||||
["ms", "Malay - Melayu"],
|
||||
["mt", "Maltese - Malti"],
|
||||
["nb", "Norwegian - Norsk Bokmål"],
|
||||
["pl", "Polish - Polski"],
|
||||
["pt", "Portuguese - Português"],
|
||||
["ro", "Romanian - Română"],
|
||||
["ru", "Russian - Русский"],
|
||||
["sk", "Slovak - Slovenčina"],
|
||||
["sl", "Slovenian - Slovenščina"],
|
||||
["es", "Spanish - Español"],
|
||||
["sv", "Swedish - Svenska"],
|
||||
["ta", "Tamil - தமிழ்"],
|
||||
["te", "Telugu - తెలుగు"],
|
||||
["th", "Thai - ไทย"],
|
||||
["tr", "Turkish - Türkçe"],
|
||||
["uk", "Ukrainian - Українська"],
|
||||
["vi", "Vietnamese - Tiếng Việt"],
|
||||
];
|
||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||
export const OPT_LANGS_SPECIAL = {
|
||||
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||
[OPT_TRANS_GOOGLE_2]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||
[OPT_TRANS_MICROSOFT]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
["zh-CN", "zh-Hans"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPL]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", ""],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLFREE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLX]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_NIUTRANS]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "cht"],
|
||||
]),
|
||||
[OPT_TRANS_VOLCENGINE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_BAIDU]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "cht"],
|
||||
["ar", "ara"],
|
||||
["bg", "bul"],
|
||||
["ca", "cat"],
|
||||
["hr", "hrv"],
|
||||
["da", "dan"],
|
||||
["fi", "fin"],
|
||||
["fr", "fra"],
|
||||
["hi", "mai"],
|
||||
["ja", "jp"],
|
||||
["ko", "kor"],
|
||||
["ms", "may"],
|
||||
["mt", "mlt"],
|
||||
["nb", "nor"],
|
||||
["ro", "rom"],
|
||||
["ru", "ru"],
|
||||
["sl", "slo"],
|
||||
["es", "spa"],
|
||||
["sv", "swe"],
|
||||
["ta", "tam"],
|
||||
["te", "tel"],
|
||||
["uk", "ukr"],
|
||||
["vi", "vie"],
|
||||
]),
|
||||
[OPT_TRANS_TENCENT]: new Map([
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh"],
|
||||
["en", "en"],
|
||||
["ar", "ar"],
|
||||
["de", "de"],
|
||||
["ru", "ru"],
|
||||
["fr", "fr"],
|
||||
["fi", "fil"],
|
||||
["ko", "ko"],
|
||||
["ms", "ms"],
|
||||
["pt", "pt"],
|
||||
["ja", "ja"],
|
||||
["th", "th"],
|
||||
["tr", "tr"],
|
||||
["es", "es"],
|
||||
["it", "it"],
|
||||
["hi", "hi"],
|
||||
["id", "id"],
|
||||
["vi", "vi"],
|
||||
]),
|
||||
[OPT_TRANS_OPENAI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OPENAI_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OPENAI_3]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_GEMINI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_GEMINI_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CLAUDE]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA_2]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_OLLAMA_3]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
||||
["auto", ""],
|
||||
["zh-CN", "chinese"],
|
||||
["zh-TW", "chinese"],
|
||||
["en", "english"],
|
||||
["ar", "arabic"],
|
||||
["de", "german"],
|
||||
["ru", "russian"],
|
||||
["fr", "french"],
|
||||
["pt", "portuguese"],
|
||||
["ja", "japanese"],
|
||||
["es", "spanish"],
|
||||
["hi", "hindi"],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE_2]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE_3]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE_4]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
[OPT_TRANS_CUSTOMIZE_5]: new Map([
|
||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||
["auto", ""],
|
||||
]),
|
||||
};
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
export const OPT_LANGS_MICROSOFT = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_MICROSOFT].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
export const OPT_LANGS_BAIDU = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
export const OPT_LANGS_TENCENT = new Map(
|
||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_TENCENT].entries()).map(([k, v]) => [
|
||||
v,
|
||||
k,
|
||||
])
|
||||
);
|
||||
OPT_LANGS_TENCENT.set("zh", "zh-CN");
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
];
|
||||
|
||||
export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
|
||||
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
|
||||
export const OPT_TIMING_MOUSEOVER = "mk_mouseover";
|
||||
export const OPT_TIMING_CONTROL = "mk_ctrlKey";
|
||||
export const OPT_TIMING_SHIFT = "mk_shiftKey";
|
||||
export const OPT_TIMING_ALT = "mk_altKey";
|
||||
export const OPT_TIMING_ALL = [
|
||||
OPT_TIMING_PAGESCROLL,
|
||||
OPT_TIMING_PAGEOPEN,
|
||||
OPT_TIMING_MOUSEOVER,
|
||||
OPT_TIMING_CONTROL,
|
||||
OPT_TIMING_SHIFT,
|
||||
OPT_TIMING_ALT,
|
||||
];
|
||||
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||
|
||||
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
|
||||
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
export const DEFAULT_TRANS_TAG = "font";
|
||||
export const DEFAULT_SELECT_STYLE =
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
|
||||
// 全局规则
|
||||
export const GLOBLA_RULE = {
|
||||
pattern: "*", // 匹配网址
|
||||
selector: DEFAULT_SELECTOR, // 选择器
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
||||
terms: "", // 专业术语
|
||||
translator: OPT_TRANS_MICROSOFT, // 翻译服务
|
||||
fromLang: "auto", // 源语言
|
||||
toLang: "zh-CN", // 目标语言
|
||||
textStyle: OPT_STYLE_DASHLINE, // 译文样式
|
||||
transOpen: "false", // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: "", // 自定义译文样式
|
||||
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
transOnly: "false", // 是否仅显示译文
|
||||
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
|
||||
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
|
||||
transTitle: "false", // 是否同时翻译页面标题
|
||||
transSelected: "true", // 是否启用划词翻译
|
||||
detectRemote: "false", // 是否使用远程语言检测
|
||||
skipLangs: [], // 不翻译的语言
|
||||
fixerSelector: "", // 修复函数选择器
|
||||
fixerFunc: "-", // 修复函数
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
transRemoveHook: "", // 钩子函数
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
||||
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 PHONIC_MAP = {
|
||||
en_phonic: ["英", "uk"],
|
||||
us_phonic: ["美", "en"],
|
||||
};
|
||||
export const OPT_TRANBOX_TRIGGER_CLICK = "click";
|
||||
export const OPT_TRANBOX_TRIGGER_HOVER = "hover";
|
||||
export const OPT_TRANBOX_TRIGGER_SELECT = "select";
|
||||
export const OPT_TRANBOX_TRIGGER_ALL = [
|
||||
OPT_TRANBOX_TRIGGER_CLICK,
|
||||
OPT_TRANBOX_TRIGGER_HOVER,
|
||||
OPT_TRANBOX_TRIGGER_SELECT,
|
||||
];
|
||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||
export const DEFAULT_TRANBOX_SETTING = {
|
||||
// transOpen: true, // 是否启用划词翻译(作废,移至rule)
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
toLang2: "en",
|
||||
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
||||
btnOffsetX: 10,
|
||||
btnOffsetY: 10,
|
||||
boxOffsetX: 0,
|
||||
boxOffsetY: 10,
|
||||
hideTranBtn: false, // 是否隐藏翻译按钮
|
||||
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||
simpleStyle: false, // 是否简洁界面
|
||||
followSelection: false, // 翻译框是否跟随选中文本
|
||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||
extStyles: "", // 附加样式
|
||||
enDict: OPT_DICT_BAIDU, // 英文词典
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_ON,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_OFF,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_HTTP_TIMEOUT = 5000; // 调用超时时间
|
||||
|
||||
// 翻译接口
|
||||
const defaultCustomApi = {
|
||||
url: "",
|
||||
key: "",
|
||||
customOption: "", // (作废)
|
||||
reqHook: "", // request 钩子函数
|
||||
resHook: "", // response 钩子函数
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
};
|
||||
const defaultOpenaiApi = {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
key: "",
|
||||
model: "gpt-4",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 256,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
};
|
||||
const defaultOllamaApi = {
|
||||
url: "http://localhost:11434/api/generate",
|
||||
key: "",
|
||||
model: "llama3.1",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
think: false,
|
||||
thinkIgnore: `qwen3,deepseek-r1`,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: "",
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
};
|
||||
export const DEFAULT_TRANS_APIS = {
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
url: URL_GOOGLE_TRAN,
|
||||
key: "",
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
apiName: OPT_TRANS_GOOGLE, // 接口自定义名称
|
||||
isDisabled: false, // 是否禁用
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT, // 超时时间
|
||||
},
|
||||
[OPT_TRANS_GOOGLE_2]: {
|
||||
url: URL_GOOGLE_TRAN2,
|
||||
key: DEFAULT_GOOGLE_API_KEY,
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_GOOGLE_2,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_MICROSOFT]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_MICROSOFT,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_BAIDU]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_BAIDU,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_TENCENT]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_TENCENT,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_VOLCENGINE]: {
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_VOLCENGINE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPL,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPLFREE]: {
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPLFREE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
url: "http://localhost:1188/translate",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_DEEPLX,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_NIUTRANS]: {
|
||||
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||
key: "",
|
||||
dictNo: "",
|
||||
memoryNo: "",
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||
apiName: OPT_TRANS_NIUTRANS,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: defaultOpenaiApi,
|
||||
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
|
||||
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||
key: "",
|
||||
model: "gemini-2.5-flash",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 2048,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_GEMINI,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_GEMINI_2]: {
|
||||
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||
key: "",
|
||||
model: "gemini-2.0-flash",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 2048,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_GEMINI_2,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_CLAUDE]: {
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
key: "",
|
||||
model: "claude-3-haiku-20240307",
|
||||
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||
temperature: 0,
|
||||
maxTokens: 1024,
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_CLAUDE,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
||||
key: "",
|
||||
fetchLimit: 1,
|
||||
fetchInterval: 500,
|
||||
apiName: OPT_TRANS_CLOUDFLAREAI,
|
||||
isDisabled: false,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||
},
|
||||
[OPT_TRANS_OLLAMA]: defaultOllamaApi,
|
||||
[OPT_TRANS_OLLAMA_2]: defaultOllamaApi,
|
||||
[OPT_TRANS_OLLAMA_3]: defaultOllamaApi,
|
||||
[OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_4]: defaultCustomApi,
|
||||
[OPT_TRANS_CUSTOMIZE_5]: defaultCustomApi,
|
||||
};
|
||||
|
||||
// 默认快捷键
|
||||
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
||||
export const OPT_SHORTCUT_STYLE = "toggleStyle";
|
||||
export const OPT_SHORTCUT_POPUP = "togglePopup";
|
||||
export const OPT_SHORTCUT_SETTING = "openSetting";
|
||||
export const DEFAULT_SHORTCUTS = {
|
||||
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
||||
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
||||
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
||||
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
|
||||
};
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
||||
export const DEFAULT_BLACKLIST = [
|
||||
"https://fishjar.github.io/kiss-translator/options.html",
|
||||
"https://translate.google.com",
|
||||
"https://www.deepl.com/translator",
|
||||
"oapi.dingtalk.com",
|
||||
"login.dingtalk.com",
|
||||
]; // 禁用翻译名单
|
||||
export const DEFAULT_CSPLIST = ["https://github.com"]; // 禁用CSP名单
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis,作废)
|
||||
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis,作废)
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
// injectWebfix: true, // 是否注入修复补丁(作废)
|
||||
// detectRemote: false, // 是否使用远程语言检测(移至rule,作废)
|
||||
// contextMenus: true, // 是否添加右键菜单(作废)
|
||||
contextMenuType: 1, // 右键菜单类型(0不显示,1简单菜单,2多级菜单)
|
||||
// transTag: DEFAULT_TRANS_TAG, // 译文元素标签(移至rule,作废)
|
||||
// transOnly: false, // 是否仅显示译文(移至rule,作废)
|
||||
// transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
||||
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||
touchTranslate: 2, // 触屏翻译
|
||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||
transInterval: 500, // 翻译间隔时间
|
||||
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
||||
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
||||
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncUser: "", // 数据同步用户名
|
||||
syncKey: "", // 数据同步密钥
|
||||
syncMeta: {}, // 数据更新及同步信息
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
dataCaches: {}, // 缓存同步时间
|
||||
};
|
||||
322
src/config/rules.js
Normal file
@@ -0,0 +1,322 @@
|
||||
import { FIXER_BR, FIXER_BN, FIXER_BR_DIV, FIXER_BN_DIV } from "../libs/webfix";
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
export const SHADOW_KEY = ">>>";
|
||||
|
||||
export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote, .kiss-p)`;
|
||||
export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre`;
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "", // 匹配网址
|
||||
selector: "", // 选择器
|
||||
keepSelector: "", // 保留元素选择器
|
||||
terms: "", // 专业术语
|
||||
translator: GLOBAL_KEY, // 翻译服务
|
||||
fromLang: GLOBAL_KEY, // 源语言
|
||||
toLang: GLOBAL_KEY, // 目标语言
|
||||
textStyle: GLOBAL_KEY, // 译文样式
|
||||
transOpen: GLOBAL_KEY, // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: "", // 自定义译文样式
|
||||
selectStyle: "", // 选择器节点样式
|
||||
parentStyle: "", // 选择器父节点样式
|
||||
injectJs: "", // 注入JS
|
||||
injectCss: "", // 注入CSS
|
||||
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||
transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译
|
||||
transTag: GLOBAL_KEY, // 译文元素标签
|
||||
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
|
||||
transSelected: GLOBAL_KEY, // 是否启用划词翻译
|
||||
detectRemote: GLOBAL_KEY, // 是否使用远程语言检测
|
||||
skipLangs: [], // 不翻译的语言
|
||||
fixerSelector: "", // 修复函数选择器
|
||||
fixerFunc: GLOBAL_KEY, // 修复函数
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
transRemoveHook: "", // 钩子函数
|
||||
};
|
||||
|
||||
const DEFAULT_DIY_STYLE = `color: #666;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
LightPink 20% 40%,
|
||||
LightSalmon 40% 60%,
|
||||
LightSeaGreen 60% 80%,
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #333;
|
||||
};`;
|
||||
|
||||
export const DEFAULT_OW_RULE = {
|
||||
translator: REMAIN_KEY,
|
||||
fromLang: REMAIN_KEY,
|
||||
toLang: REMAIN_KEY,
|
||||
textStyle: REMAIN_KEY,
|
||||
transOpen: REMAIN_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
const RULES_MAP = {
|
||||
"www.google.com/search": {
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
},
|
||||
"news.google.com": {
|
||||
selector: `[data-n-tid], ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.foxnews.com": {
|
||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||
},
|
||||
"bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": {
|
||||
selector: `${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"themessenger.com": {
|
||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.telegraph.co.uk, go.dev/doc/": {
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.theguardian.com": {
|
||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
||||
},
|
||||
"www.semafor.com": {
|
||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
||||
},
|
||||
"www.noemamag.com": {
|
||||
selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
|
||||
},
|
||||
"restofworld.org": {
|
||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
||||
},
|
||||
"www.axios.com": {
|
||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.newyorker.com": {
|
||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
|
||||
},
|
||||
"time.com": {
|
||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
||||
},
|
||||
"www.dw.com": {
|
||||
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.bbc.com": {
|
||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
|
||||
},
|
||||
"www.chinadaily.com.cn": {
|
||||
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.facebook.com": {
|
||||
selector: `[role="main"] [dir="auto"]`,
|
||||
},
|
||||
"www.reddit.com, new.reddit.com, sh.reddit.com": {
|
||||
selector: `:is(#AppRouter-main-content, #overlayScrollContainer) :is([class^=tbIA],[class^=_1zP],[class^=ULWj],[class^=_2Jj], [class^=_334],[class^=_2Gr],[class^=_7T4],[class^=_1WO], ${DEFAULT_SELECTOR}); [id^="post-title"], :is([slot="text-body"], [slot="comment"]) ${DEFAULT_SELECTOR}, recent-posts h3, aside :is(span:has(>h2), p); shreddit-subreddit-header >>> :is(#title, #description)`,
|
||||
},
|
||||
"www.quora.com": {
|
||||
selector: `.qu-wordBreak--break-word`,
|
||||
},
|
||||
"edition.cnn.com": {
|
||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.reuters.com": {
|
||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.bloomberg.com": {
|
||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"deno.land, docs.github.com": {
|
||||
selector: `main ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"doc.rust-lang.org": {
|
||||
selector: `.content ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"www.indiehackers.com": {
|
||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
||||
},
|
||||
"platform.openai.com/docs": {
|
||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"en.wikipedia.org": {
|
||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: `.mwe-math-element`,
|
||||
},
|
||||
"stackoverflow.com, serverfault.com, superuser.com, stackexchange.com, askubuntu.com, stackapps.com, mathoverflow.net":
|
||||
{
|
||||
selector: `.s-prose ${DEFAULT_SELECTOR}, .comment-copy, .question-hyperlink, .s-post-summary--content-title, .s-post-summary--content-excerpt`,
|
||||
keepSelector: `${DEFAULT_KEEP_SELECTOR}, .math-container`,
|
||||
},
|
||||
"www.npmjs.com/package, developer.chrome.com/docs, medium.com, react.dev, create-react-app.dev, pytorch.org":
|
||||
{
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"news.ycombinator.com": {
|
||||
selector: `.title, p`,
|
||||
fixerSelector: `.toptext, .commtext`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"github.com": {
|
||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"twitter.com": {
|
||||
selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
|
||||
keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
|
||||
},
|
||||
"m.youtube.com": {
|
||||
selector: `.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
|
||||
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
keepSelector: `img, #content-text>a`,
|
||||
},
|
||||
"www.youtube.com": {
|
||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
|
||||
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||
keepSelector: `img, #content-text>a`,
|
||||
},
|
||||
"bard.google.com": {
|
||||
selector: `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.bing.com, copilot.microsoft.com": {
|
||||
selector: `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
|
||||
},
|
||||
"www.phoronix.com": {
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
fixerSelector: `.content`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"wx2.qq.com": {
|
||||
selector: `.js_message_plain`,
|
||||
},
|
||||
"app.slack.com/client/": {
|
||||
selector: `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
|
||||
},
|
||||
"discord.com/channels/": {
|
||||
selector: `div[class^=message], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content], div[id^=message]`,
|
||||
keepSelector: `li[class^='card'] div[class^='message'], [class^='embedFieldValue'], [data-list-item-id^='forum-channel-list'] div[class^='headerText']`,
|
||||
},
|
||||
"t.me/s/": {
|
||||
selector: `.js-message_text ${DEFAULT_SELECTOR}`,
|
||||
fixerSelector: `.tgme_widget_message_text`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"web.telegram.org/k": {
|
||||
selector: `div.kiss-p`,
|
||||
keepSelector: `div[class^=time], .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element, reactions-element`,
|
||||
fixerSelector: `.message`,
|
||||
fixerFunc: FIXER_BN_DIV,
|
||||
},
|
||||
"web.telegram.org/a": {
|
||||
selector: `.text-content > .kiss-p`,
|
||||
keepSelector: `.Reactions, .time, .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element`,
|
||||
fixerSelector: `.text-content`,
|
||||
fixerFunc: FIXER_BR_DIV,
|
||||
},
|
||||
"www.instagram.com/": {
|
||||
selector: `h1, article span[dir=auto] > span[dir=auto], ._ab1y`,
|
||||
},
|
||||
"www.instagram.com/p/,www.instagram.com/reels/": {
|
||||
selector: `h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
|
||||
},
|
||||
"mail.google.com": {
|
||||
selector: `.a3s.aiL ${DEFAULT_SELECTOR}, span[data-thread-id]`,
|
||||
fixerSelector: `.a3s.aiL`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"web.whatsapp.com": {
|
||||
selector: `.copyable-text > span`,
|
||||
},
|
||||
"chat.openai.com": {
|
||||
selector: `div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
|
||||
fixerSelector: `div[data-message-author-role='user'] > div`,
|
||||
fixerFunc: FIXER_BN,
|
||||
},
|
||||
"forum.ru-board.com": {
|
||||
selector: `.tit, .dats, .kiss-p, .lgf ${DEFAULT_SELECTOR}`,
|
||||
fixerSelector: `span.post`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"education.github.com": {
|
||||
selector: `${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
|
||||
},
|
||||
"blogs.windows.com": {
|
||||
selector: `${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`,
|
||||
fixerSelector: `.t-content>div>ul>li`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"developer.apple.com/documentation/": {
|
||||
selector: `#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"greasyfork.org": {
|
||||
selector: `h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.fmkorea.com": {
|
||||
selector: `#container ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"forum.arduino.cc": {
|
||||
selector: `.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"docs.arduino.cc": {
|
||||
selector: `[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"www.historydefined.net": {
|
||||
selector: `.wp-element-caption, ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"gobyexample.com": {
|
||||
selector: `.docs p`,
|
||||
keepSelector: `code`,
|
||||
},
|
||||
"go.dev/tour": {
|
||||
selector: `#left-side ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: `code, img, svg >>> code`,
|
||||
},
|
||||
"pkg.go.dev": {
|
||||
selector: `.Documentation-content ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: `${DEFAULT_KEEP_SELECTOR}, a, span`,
|
||||
},
|
||||
"docs.rs": {
|
||||
selector: `.docblock ${DEFAULT_SELECTOR}, .docblock-short`,
|
||||
keepSelector: `code >>> code`,
|
||||
},
|
||||
"randomnerdtutorials.com": {
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
"notebooks.githubusercontent.com/view/ipynb": {
|
||||
selector: `#notebook-container ${DEFAULT_SELECTOR}`,
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||
},
|
||||
"developers.cloudflare.com": {
|
||||
selector: `article ${DEFAULT_SELECTOR}, .WorkerStarter--description`,
|
||||
keepSelector: `a[rel='noopener'], code`,
|
||||
},
|
||||
"ubuntuforums.org": {
|
||||
fixerSelector: `.postcontent`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"play.google.com/store/apps/details": {
|
||||
fixerSelector: `[data-g-id="description"]`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
"news.yahoo.co.jp/articles/": {
|
||||
fixerSelector: `.sc-cTsKDU`,
|
||||
fixerFunc: FIXER_BN,
|
||||
},
|
||||
"chromereleases.googleblog.com": {
|
||||
fixerSelector: `.post-content, .post-content > span, li > span`,
|
||||
fixerFunc: FIXER_BR,
|
||||
},
|
||||
};
|
||||
|
||||
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([pattern, rule]) => ({
|
||||
...DEFAULT_RULE,
|
||||
...rule,
|
||||
pattern,
|
||||
}));
|
||||
3
src/content.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { run } from "./common";
|
||||
|
||||
run();
|
||||
60
src/hooks/Alert.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createContext, useContext, useState, forwardRef } from "react";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
|
||||
const Alert = forwardRef(function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||
});
|
||||
|
||||
const AlertContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 左下角提示,注入context后,方便全局调用
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function AlertProvider({ children }) {
|
||||
const vertical = "top";
|
||||
const horizontal = "center";
|
||||
const [open, setOpen] = useState(false);
|
||||
const [severity, setSeverity] = useState("info");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const showAlert = (msg, type) => {
|
||||
setOpen(true);
|
||||
setMessage(msg);
|
||||
setSeverity(type);
|
||||
};
|
||||
|
||||
const handleClose = (_, reason) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const error = (msg) => showAlert(msg, "error");
|
||||
const warning = (msg) => showAlert(msg, "warning");
|
||||
const info = (msg) => showAlert(msg, "info");
|
||||
const success = (msg) => showAlert(msg, "success");
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAlert() {
|
||||
return useContext(AlertContext);
|
||||
}
|
||||
34
src/hooks/Api.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useApi(translator) {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
|
||||
|
||||
const updateApi = useCallback(
|
||||
async (obj) => {
|
||||
const api = {
|
||||
...DEFAULT_TRANS_APIS[translator],
|
||||
...(transApis[translator] || {}),
|
||||
};
|
||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
||||
await updateSetting({ transApis });
|
||||
},
|
||||
[translator, transApis, updateSetting]
|
||||
);
|
||||
|
||||
const resetApi = useCallback(async () => {
|
||||
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
|
||||
await updateSetting({ transApis });
|
||||
}, [translator, transApis, updateSetting]);
|
||||
|
||||
return {
|
||||
api: {
|
||||
...DEFAULT_TRANS_APIS[translator],
|
||||
...(transApis[translator] || {}),
|
||||
},
|
||||
updateApi,
|
||||
resetApi,
|
||||
};
|
||||
}
|
||||
61
src/hooks/Audio.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { apiBaiduTTS } from "../apis";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 声音播放hook
|
||||
* @param {*} src
|
||||
* @returns
|
||||
*/
|
||||
export function useAudio(src) {
|
||||
const audioRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const onPlay = useCallback(() => {
|
||||
audioRef.current?.play();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
const audio = new Audio(src);
|
||||
audio.addEventListener("error", (err) => setError(err));
|
||||
audio.addEventListener("canplaythrough", () => setReady(true));
|
||||
audio.addEventListener("play", () => setPlaying(true));
|
||||
audio.addEventListener("ended", () => setPlaying(false));
|
||||
audioRef.current = audio;
|
||||
}, [src]);
|
||||
|
||||
return {
|
||||
error,
|
||||
ready,
|
||||
playing,
|
||||
onPlay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语音hook
|
||||
* @param {*} text
|
||||
* @param {*} lan
|
||||
* @param {*} spd
|
||||
* @returns
|
||||
*/
|
||||
export function useTextAudio(text, lan = "uk", spd = 3) {
|
||||
const [src, setSrc] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setSrc(await apiBaiduTTS(text, lan, spd));
|
||||
} catch (err) {
|
||||
kissLog(err, "baidu tts");
|
||||
}
|
||||
})();
|
||||
}, [text, lan, spd]);
|
||||
|
||||
return useAudio(src);
|
||||
}
|
||||
19
src/hooks/ColorMode.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
/**
|
||||
* 深色模式hook
|
||||
* @returns
|
||||
*/
|
||||
export function useDarkMode() {
|
||||
const {
|
||||
setting: { darkMode },
|
||||
updateSetting,
|
||||
} = useSetting();
|
||||
|
||||
const toggleDarkMode = useCallback(async () => {
|
||||
await updateSetting({ darkMode: !darkMode });
|
||||
}, [darkMode, updateSetting]);
|
||||
|
||||
return { darkMode, toggleDarkMode };
|
||||
}
|
||||
11
src/hooks/Fab.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { STOKEY_FAB } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
/**
|
||||
* fab hook
|
||||
* @returns
|
||||
*/
|
||||
export function useFab() {
|
||||
const { data, update } = useStorage(STOKEY_FAB);
|
||||
return { fab: data, updateFab: update };
|
||||
}
|
||||
68
src/hooks/FavWords.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { KV_WORDS_KEY } from "../config";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { trySyncWords } from "../libs/sync";
|
||||
import { getWordsWithDefault, setWords } from "../libs/storage";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
export function useFavWords() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [favWords, setFavWords] = useState({});
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const toggleFav = useCallback(
|
||||
async (word) => {
|
||||
const favs = { ...favWords };
|
||||
if (favs[word]) {
|
||||
delete favs[word];
|
||||
} else {
|
||||
favs[word] = { createdAt: Date.now() };
|
||||
}
|
||||
await setWords(favs);
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords(favs);
|
||||
},
|
||||
[updateSyncMeta, favWords]
|
||||
);
|
||||
|
||||
const mergeWords = useCallback(
|
||||
async (newWords) => {
|
||||
const favs = { ...favWords };
|
||||
newWords.forEach((word) => {
|
||||
if (!favs[word]) {
|
||||
favs[word] = { createdAt: Date.now() };
|
||||
}
|
||||
});
|
||||
await setWords(favs);
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords(favs);
|
||||
},
|
||||
[updateSyncMeta, favWords]
|
||||
);
|
||||
|
||||
const clearWords = useCallback(async () => {
|
||||
await setWords({});
|
||||
await updateSyncMeta(KV_WORDS_KEY);
|
||||
await trySyncWords();
|
||||
setFavWords({});
|
||||
}, [updateSyncMeta]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await trySyncWords();
|
||||
const favWords = await getWordsWithDefault();
|
||||
setFavWords(favWords);
|
||||
} catch (err) {
|
||||
kissLog(err, "query fav");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return { loading, favWords, toggleFav, mergeWords, clearWords };
|
||||
}
|
||||
40
src/hooks/Fetch.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* fetch data hook
|
||||
* @returns
|
||||
*/
|
||||
export const useFetch = (url) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`[${res.status}] ${res.statusText}`);
|
||||
}
|
||||
let data;
|
||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
setData(data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [url]);
|
||||
|
||||
return [data, loading, error];
|
||||
};
|
||||
29
src/hooks/I18n.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useSetting } from "./Setting";
|
||||
import { I18N, URL_RAW_PREFIX } from "../config";
|
||||
import { useFetch } from "./Fetch";
|
||||
|
||||
export const getI18n = (uiLang, key, defaultText = "") => {
|
||||
return I18N?.[key]?.[uiLang] ?? defaultText;
|
||||
};
|
||||
|
||||
export const useLangMap = (uiLang) => {
|
||||
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
|
||||
};
|
||||
|
||||
/**
|
||||
* 多语言 hook
|
||||
* @returns
|
||||
*/
|
||||
export const useI18n = () => {
|
||||
const {
|
||||
setting: { uiLang },
|
||||
} = useSetting();
|
||||
return useLangMap(uiLang);
|
||||
};
|
||||
|
||||
export const useI18nMd = (key) => {
|
||||
const i18n = useI18n();
|
||||
const fileName = i18n(key);
|
||||
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : "";
|
||||
return useFetch(url);
|
||||
};
|
||||
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 };
|
||||
}
|
||||
91
src/hooks/Rules.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncRules } from "../libs/sync";
|
||||
import { checkRules } from "../libs/rules";
|
||||
import { useCallback } from "react";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
/**
|
||||
* 规则 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useRules() {
|
||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const updateRules = useCallback(
|
||||
async (rules) => {
|
||||
await save(rules);
|
||||
await updateSyncMeta(KV_RULES_KEY);
|
||||
trySyncRules();
|
||||
},
|
||||
[save, updateSyncMeta]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rule.pattern === "*") {
|
||||
return;
|
||||
}
|
||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
async (pattern) => {
|
||||
let rules = [...list];
|
||||
if (pattern === "*") {
|
||||
return;
|
||||
}
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const clear = useCallback(async () => {
|
||||
let rules = [...list];
|
||||
rules = rules.filter((item) => item.pattern === "*");
|
||||
await updateRules(rules);
|
||||
}, [list, updateRules]);
|
||||
|
||||
const put = useCallback(
|
||||
async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
if (pattern === "*") {
|
||||
obj.pattern = "*";
|
||||
}
|
||||
const rule = rules.find((r) => r.pattern === pattern);
|
||||
rule && Object.assign(rule, obj);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const merge = useCallback(
|
||||
async (newRules) => {
|
||||
const rules = [...list];
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find(
|
||||
(oldRule) => oldRule.pattern === newRule.pattern
|
||||
);
|
||||
if (rule) {
|
||||
Object.assign(rule, newRule);
|
||||
} else {
|
||||
rules.unshift(newRule);
|
||||
}
|
||||
});
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
return { list, add, del, clear, put, merge };
|
||||
}
|
||||
58
src/hooks/Setting.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncSetting } from "../libs/sync";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { debounce } from "../libs/utils";
|
||||
import { useSyncMeta } from "./Sync";
|
||||
|
||||
const SettingContext = createContext({
|
||||
setting: null,
|
||||
updateSetting: async () => {},
|
||||
reloadSetting: async () => {},
|
||||
});
|
||||
|
||||
export function SettingProvider({ children }) {
|
||||
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
const { updateSyncMeta } = useSyncMeta();
|
||||
|
||||
const syncSetting = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
trySyncSetting();
|
||||
}, [2000]),
|
||||
[]
|
||||
);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
async (obj) => {
|
||||
await update(obj);
|
||||
await updateSyncMeta(KV_SETTING_KEY);
|
||||
syncSetting();
|
||||
},
|
||||
[update, syncSetting, updateSyncMeta]
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContext.Provider
|
||||
value={{
|
||||
setting: data,
|
||||
updateSetting,
|
||||
reloadSetting: reload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSetting() {
|
||||
return useContext(SettingContext);
|
||||
}
|
||||
19
src/hooks/Shortcut.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_SHORTCUTS } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useShortcut(action) {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
||||
const shortcut = shortcuts[action] || [];
|
||||
|
||||
const setShortcut = useCallback(
|
||||
async (val) => {
|
||||
Object.assign(shortcuts, { [action]: val });
|
||||
await updateSetting({ shortcuts });
|
||||
},
|
||||
[action, shortcuts, updateSetting]
|
||||
);
|
||||
|
||||
return { shortcut, setShortcut };
|
||||
}
|
||||
70
src/hooks/Storage.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { storage } from "../libs/storage";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} key
|
||||
* @param {*} defaultVal 需为调用hook外的常量
|
||||
* @returns
|
||||
*/
|
||||
export function useStorage(key, defaultVal) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const save = useCallback(
|
||||
async (val) => {
|
||||
setData(val);
|
||||
await storage.setObj(key, val);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
async (obj) => {
|
||||
setData((pre = {}) => ({ ...pre, ...obj }));
|
||||
await storage.putObj(key, obj);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
setData(null);
|
||||
await storage.del(key);
|
||||
}, [key]);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "storage reload");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
} else if (defaultVal) {
|
||||
setData(defaultVal);
|
||||
await storage.setObj(key, defaultVal);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "storage load");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [key, defaultVal]);
|
||||
|
||||
return { data, save, update, remove, reload, loading };
|
||||
}
|
||||
114
src/hooks/SubRules.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||
import { delSubRules } from "../libs/storage";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useSubRules() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRules, setSelectedRules] = useState([]);
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
|
||||
|
||||
const selectedSub = useMemo(() => list.find((item) => item.selected), [list]);
|
||||
const selectedUrl = selectedSub.url;
|
||||
|
||||
const selectSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
item.selected = true;
|
||||
} else {
|
||||
item.selected = false;
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const updateSub = useCallback(
|
||||
async (url, obj) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
Object.assign(item, obj);
|
||||
}
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const addSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.push({ url, selected: false });
|
||||
await updateSetting({ subrulesList });
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
const delSub = useCallback(
|
||||
async (url) => {
|
||||
let subrulesList = [...list];
|
||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||
await updateSetting({ subrulesList });
|
||||
await delSubRules(url);
|
||||
},
|
||||
[list, updateSetting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedUrl) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||
setSelectedRules(rules);
|
||||
} catch (err) {
|
||||
kissLog(err, "loadOrFetchSubRules");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [selectedUrl]);
|
||||
|
||||
return {
|
||||
subList: list,
|
||||
selectSub,
|
||||
updateSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedSub,
|
||||
selectedUrl,
|
||||
selectedRules,
|
||||
setSelectedRules,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆写订阅规则
|
||||
* @returns
|
||||
*/
|
||||
export function useOwSubRule() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const { owSubrule = DEFAULT_OW_RULE } = setting;
|
||||
|
||||
const updateOwSubrule = useCallback(
|
||||
async (obj) => {
|
||||
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
|
||||
},
|
||||
[owSubrule, updateSetting]
|
||||
);
|
||||
|
||||
return { owSubrule, updateOwSubrule };
|
||||
}
|
||||
63
src/hooks/Sync.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from "react";
|
||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
/**
|
||||
* sync hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSync() {
|
||||
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update, reloadSync: reload };
|
||||
}
|
||||
|
||||
/**
|
||||
* update syncmeta hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSyncMeta() {
|
||||
const { sync, updateSync } = useSync();
|
||||
const updateSyncMeta = useCallback(
|
||||
async (key) => {
|
||||
const syncMeta = sync?.syncMeta || {};
|
||||
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||
await updateSync({ syncMeta });
|
||||
},
|
||||
[sync?.syncMeta, updateSync]
|
||||
);
|
||||
return { updateSyncMeta };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
45
src/hooks/Theme.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useMemo } from "react";
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { useDarkMode } from "./ColorMode";
|
||||
import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
|
||||
/**
|
||||
* mui 主题配置
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function Theme({ children, options, styles }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
const theme = useMemo(() => {
|
||||
let htmlFontSize = 16;
|
||||
try {
|
||||
const s = window.getComputedStyle(document.body.parentNode).fontSize;
|
||||
const fontSize = parseInt(s.replace("px", ""));
|
||||
if (fontSize > 0 && fontSize < 1000) {
|
||||
htmlFontSize = fontSize;
|
||||
}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
||||
},
|
||||
typography: {
|
||||
htmlFontSize,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}, [darkMode, options]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||
<CssBaseline />
|
||||
<GlobalStyles styles={styles} />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
18
src/hooks/Tranbox.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useTranbox() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
||||
|
||||
const updateTranbox = useCallback(
|
||||
async (obj) => {
|
||||
Object.assign(tranboxSetting, obj);
|
||||
await updateSetting({ tranboxSetting });
|
||||
},
|
||||
[tranboxSetting, updateSetting]
|
||||
);
|
||||
|
||||
return { tranboxSetting, updateTranbox };
|
||||
}
|
||||
66
src/hooks/Translate.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 翻译hook
|
||||
* @param {*} q
|
||||
* @param {*} rule
|
||||
* @param {*} setting
|
||||
* @returns
|
||||
*/
|
||||
export function useTranslate(q, rule, setting) {
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sameLang, setSamelang] = useState(false);
|
||||
|
||||
const { translator, fromLang, toLang, detectRemote, skipLangs = [] } = rule;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (!q.replace(/\[(\d+)\]/g, "").trim()) {
|
||||
setText(q);
|
||||
setSamelang(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let deLang = "";
|
||||
if (fromLang === "auto") {
|
||||
deLang = await tryDetectLang(
|
||||
q,
|
||||
detectRemote === "true",
|
||||
setting.langDetector
|
||||
);
|
||||
}
|
||||
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
text: q,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting: {
|
||||
...DEFAULT_TRANS_APIS[translator],
|
||||
...(setting.transApis[translator] || {}),
|
||||
},
|
||||
});
|
||||
setText(trText);
|
||||
setSamelang(isSame);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "translate");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [q, translator, fromLang, toLang, detectRemote, skipLangs, setting]);
|
||||
|
||||
return { text, sameLang, loading };
|
||||
}
|
||||
61
src/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import { useFetch } from "./hooks/Fetch";
|
||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||
|
||||
function App() {
|
||||
const [lang, setLang] = useState("zh");
|
||||
const [data, loading, error] = useFetch(
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||
);
|
||||
return (
|
||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setLang((pre) => (pre === "zh" ? "en" : "zh"));
|
||||
}}
|
||||
>
|
||||
{lang === "zh" ? "ENGLISH" : "中文"}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install/Update Userscript for iOS Safari
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
<ReactMarkdown children={error ? error.message : data} />
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
45
src/libs/auth.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { getMsauth, setMsauth } from "./storage";
|
||||
import { URL_MICROSOFT_AUTH } from "../config";
|
||||
import { fetchHandle } from "./fetch";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
const parseMSToken = (token) => {
|
||||
try {
|
||||
return JSON.parse(atob(token.split(".")[1])).exp;
|
||||
} catch (err) {
|
||||
kissLog(err, "parseMSToken");
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 闭包缓存token,减少对storage查询
|
||||
* @returns
|
||||
*/
|
||||
const _msAuth = () => {
|
||||
let { token, exp } = {};
|
||||
|
||||
return async () => {
|
||||
// 查询内存缓存
|
||||
const now = Date.now();
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
}
|
||||
|
||||
// 查询storage缓存
|
||||
const res = await getMsauth();
|
||||
token = res?.token;
|
||||
exp = res?.exp;
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
}
|
||||
|
||||
// 缓存没有或失效,查询接口
|
||||
token = await fetchHandle({ input: URL_MICROSOFT_AUTH });
|
||||
exp = parseMSToken(token);
|
||||
await setMsauth({ token, exp });
|
||||
return [token, exp];
|
||||
};
|
||||
};
|
||||
|
||||
export const msAuth = _msAuth();
|
||||
10
src/libs/blacklist.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { isMatch } from "./utils";
|
||||
|
||||
/**
|
||||
* 检查是否在黑名单中
|
||||
* @param {*} href
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const isInBlacklist = (href, { blacklist }) =>
|
||||
blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
|
||||
17
src/libs/browser.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
/**
|
||||
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
|
||||
* @returns
|
||||
*/
|
||||
function _browser() {
|
||||
try {
|
||||
return require("webextension-polyfill");
|
||||
} catch (err) {
|
||||
// kissLog(err, "browser");
|
||||
}
|
||||
}
|
||||
|
||||
export const browser = _browser();
|
||||
|
||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||
6
src/libs/client.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
|
||||
export const client = process.env.REACT_APP_CLIENT;
|
||||
export const isExt = CLIENT_EXTS.includes(client);
|
||||
export const isGm = client === CLIENT_USERSCRIPT;
|
||||
export const isWeb = client === CLIENT_WEB;
|
||||
319
src/libs/fetch.js
Normal file
@@ -0,0 +1,319 @@
|
||||
import { isExt, isGm } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { taskPool } from "./pool";
|
||||
import { getSettingWithDefault } from "./storage";
|
||||
|
||||
import {
|
||||
MSG_FETCH,
|
||||
MSG_GET_HTTPCACHE,
|
||||
CACHE_NAME,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
} from "../config";
|
||||
import { isBg } from "./browser";
|
||||
import { genTransReq } from "../apis/trans";
|
||||
import { kissLog } from "./log";
|
||||
import { blobToBase64 } from "./utils";
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
const newCacheReq = async (input, init) => {
|
||||
let request = new Request(input, init);
|
||||
if (request.method !== "GET") {
|
||||
const body = await request.text();
|
||||
const cacheUrl = new URL(request.url);
|
||||
cacheUrl.pathname += body;
|
||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* 油猴脚本的请求封装
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const fetchGM = async (
|
||||
input,
|
||||
{ method = "GET", headers, body, timeout } = {}
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
GM.xmlHttpRequest({
|
||||
method,
|
||||
url: input,
|
||||
headers,
|
||||
data: body,
|
||||
// withCredentials: true,
|
||||
timeout,
|
||||
onload: ({ response, responseHeaders, status, statusText }) => {
|
||||
const headers = {};
|
||||
responseHeaders.split("\n").forEach((line) => {
|
||||
const [name, value] = line.split(":").map((item) => item.trim());
|
||||
if (name && value) {
|
||||
headers[name] = value;
|
||||
}
|
||||
});
|
||||
resolve({
|
||||
body: response,
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
},
|
||||
onerror: reject,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
|
||||
if (transOpts?.translator) {
|
||||
[input, init] = await genTransReq(transOpts, apiSetting);
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
throw new Error("url is empty");
|
||||
}
|
||||
|
||||
let timeout = apiSetting?.httpTimeout || DEFAULT_HTTP_TIMEOUT;
|
||||
if (!apiSetting) {
|
||||
try {
|
||||
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
if (isGm) {
|
||||
// let info;
|
||||
// if (window.KISS_GM) {
|
||||
// info = await window.KISS_GM.getInfo();
|
||||
// } else {
|
||||
// info = GM.info;
|
||||
// }
|
||||
|
||||
// Tampermonkey --> .connects
|
||||
// Violentmonkey --> .connect
|
||||
// const connects = info?.script?.connects || info?.script?.connect || [];
|
||||
// const url = new URL(input);
|
||||
// const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||
|
||||
// if (isSafe) {
|
||||
// // todo: 自定义接口 init 可能包含了 signal
|
||||
// Object.assign(init, { timeout });
|
||||
|
||||
// const { body, headers, status, statusText } = window.KISS_GM
|
||||
// ? await window.KISS_GM.fetch(input, init)
|
||||
// : await fetchGM(input, init);
|
||||
|
||||
// return new Response(body, {
|
||||
// headers: new Headers(headers),
|
||||
// status,
|
||||
// statusText,
|
||||
// });
|
||||
// }
|
||||
|
||||
// todo: 自定义接口 init 可能包含了 signal
|
||||
Object.assign(init, { timeout });
|
||||
|
||||
const { body, headers, status, statusText } = window.KISS_GM
|
||||
? await window.KISS_GM.fetch(input, init)
|
||||
: await fetchGM(input, init);
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(headers),
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
}
|
||||
|
||||
if (AbortSignal?.timeout && !init.signal) {
|
||||
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
|
||||
}
|
||||
|
||||
return fetch(input, init);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 response
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const parseResponse = async (res) => {
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
if (contentType?.includes("json")) {
|
||||
return await res.json();
|
||||
} else if (contentType?.includes("audio")) {
|
||||
const blob = await res.blob();
|
||||
return await blobToBase64(blob);
|
||||
}
|
||||
return await res.text();
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询 caches
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const getHttpCache = async (input, { method, headers, body }) => {
|
||||
try {
|
||||
const req = await newCacheReq(input, { method, headers, body });
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const res = await cache.match(req);
|
||||
return parseResponse(res);
|
||||
} catch (err) {
|
||||
kissLog(err, "get cache");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 插入 caches
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @param {*} res
|
||||
*/
|
||||
export const putHttpCache = async (input, { method, headers, body }, res) => {
|
||||
try {
|
||||
const req = await newCacheReq(input, { method, headers, body });
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.put(req, res);
|
||||
} catch (err) {
|
||||
kissLog(err, "put cache");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理请求
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const fetchHandle = async ({
|
||||
input,
|
||||
useCache,
|
||||
transOpts,
|
||||
apiSetting,
|
||||
...init
|
||||
}) => {
|
||||
// 发送请求
|
||||
const res = await fetchPatcher(input, init, transOpts, apiSetting);
|
||||
if (!res) {
|
||||
throw new Error("Unknow error");
|
||||
} else if (!res.ok) {
|
||||
const msg = {
|
||||
url: res.url,
|
||||
status: res.status,
|
||||
};
|
||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
||||
msg.response = await res.json();
|
||||
}
|
||||
throw new Error(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
await putHttpCache(input, init, res.clone());
|
||||
}
|
||||
|
||||
return parseResponse(res);
|
||||
};
|
||||
|
||||
/**
|
||||
* fetch 兼容性封装
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPolyfill = (args) => {
|
||||
// 插件
|
||||
if (isExt && !isBg()) {
|
||||
return sendBgMsg(MSG_FETCH, args);
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return fetchHandle(args);
|
||||
};
|
||||
|
||||
/**
|
||||
* getHttpCache 兼容性封装
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const getHttpCachePolyfill = (input, init) => {
|
||||
// 插件
|
||||
if (isExt && !isBg()) {
|
||||
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return getHttpCache(input, init);
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求池实例
|
||||
*/
|
||||
export const fetchPool = taskPool(
|
||||
fetchPolyfill,
|
||||
null,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT
|
||||
);
|
||||
|
||||
/**
|
||||
* 数据请求
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const fetchData = async (input, { useCache, usePool, ...args } = {}) => {
|
||||
if (!input?.trim()) {
|
||||
throw new Error("URL is empty");
|
||||
}
|
||||
|
||||
// 查询缓存
|
||||
if (useCache) {
|
||||
const cache = await getHttpCachePolyfill(input, args);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
// 通过任务池发送请求
|
||||
if (usePool) {
|
||||
return fetchPool.push({ input, useCache, ...args });
|
||||
}
|
||||
|
||||
// 直接请求
|
||||
return fetchPolyfill({ input, useCache, ...args });
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 fetch pool 参数
|
||||
* @param {*} interval
|
||||
* @param {*} limit
|
||||
*/
|
||||
export const updateFetchPool = (interval, limit) => {
|
||||
fetchPool.update(interval, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空任务池
|
||||
*/
|
||||
export const clearFetchPool = () => {
|
||||
fetchPool.clear();
|
||||
};
|
||||
102
src/libs/gm.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { fetchGM } from "./fetch";
|
||||
import { genEventName } from "./utils";
|
||||
|
||||
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
|
||||
const MSG_GM_setValue = "setValue";
|
||||
const MSG_GM_getValue = "getValue";
|
||||
const MSG_GM_deleteValue = "deleteValue";
|
||||
const MSG_GM_info = "info";
|
||||
|
||||
/**
|
||||
* 注入页面的脚本,请求并接受GM接口信息
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const injectScript = (ping) => {
|
||||
window.APP_INFO = {
|
||||
name: process.env.REACT_APP_NAME,
|
||||
version: process.env.REACT_APP_VERSION,
|
||||
eventName: ping,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 适配GM脚本
|
||||
*/
|
||||
export const adaptScript = (ping) => {
|
||||
const promiseGM = (action, args, timeout = 5000) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const pong = genEventName();
|
||||
const handleEvent = (e) => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
const { data, error } = e.detail;
|
||||
if (error) {
|
||||
reject(new Error(error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(pong, handleEvent);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ping, { detail: { action, args, pong } })
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener(pong, handleEvent);
|
||||
reject(new Error("timeout"));
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
window.KISS_GM = {
|
||||
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
|
||||
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
|
||||
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
|
||||
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
|
||||
getInfo: async () => {
|
||||
if (!window.GM_info) {
|
||||
window.GM_info = await promiseGM(MSG_GM_info);
|
||||
}
|
||||
return window.GM_info;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听并回应页面对GM接口的请求
|
||||
* @param {*} param0
|
||||
*/
|
||||
export const handlePing = async (e) => {
|
||||
const { action, args, pong } = e.detail;
|
||||
let res;
|
||||
try {
|
||||
switch (action) {
|
||||
case MSG_GM_xmlHttpRequest:
|
||||
const { input, init } = args;
|
||||
res = await fetchGM(input, init);
|
||||
break;
|
||||
case MSG_GM_setValue:
|
||||
const { key, val } = args;
|
||||
await GM.setValue(key, val);
|
||||
res = val;
|
||||
break;
|
||||
case MSG_GM_getValue:
|
||||
res = await GM.getValue(args.key);
|
||||
break;
|
||||
case MSG_GM_deleteValue:
|
||||
await GM.deleteValue(args.key);
|
||||
res = "ok";
|
||||
break;
|
||||
case MSG_GM_info:
|
||||
res = GM.info;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`message action is unavailable: ${action}`);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
|
||||
} catch (err) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(pong, { detail: { error: err.message } })
|
||||
);
|
||||
}
|
||||
};
|
||||
11
src/libs/iframe.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const isIframe = window.self !== window.top;
|
||||
|
||||
export const sendIframeMsg = (action, args) => {
|
||||
document.querySelectorAll("iframe").forEach((iframe) => {
|
||||
iframe.contentWindow.postMessage({ action, args }, "*");
|
||||
});
|
||||
};
|
||||
|
||||
export const sendParentMsg = (action, args) => {
|
||||
window.parent.postMessage({ action, args }, "*");
|
||||
};
|
||||
65
src/libs/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import {
|
||||
apiGoogleLangdetect,
|
||||
apiMicrosoftLangdetect,
|
||||
apiBaiduLangdetect,
|
||||
apiTencentLangdetect,
|
||||
} from "../apis";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
const langdetectMap = {
|
||||
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
|
||||
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
*/
|
||||
export const tryClearCaches = async () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
} catch (err) {
|
||||
kissLog(err, "clean caches");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 语言识别
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const tryDetectLang = async (
|
||||
q,
|
||||
useRemote = false,
|
||||
langDetector = OPT_TRANS_MICROSOFT
|
||||
) => {
|
||||
let lang = "";
|
||||
|
||||
if (useRemote) {
|
||||
try {
|
||||
lang = await langdetectMap[langDetector](q);
|
||||
} catch (err) {
|
||||
kissLog(err, "detect lang remote");
|
||||
}
|
||||
}
|
||||
|
||||
if (!lang) {
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(q);
|
||||
lang = res?.languages?.[0]?.language;
|
||||
} catch (err) {
|
||||
kissLog(err, "detect lang local");
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
};
|
||||
35
src/libs/injector.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Function to inject inline JavaScript code
|
||||
export const injectInlineJs = (code) => {
|
||||
const el = document.createElement("script");
|
||||
el.setAttribute("data-source", "KISS-Calendar injectInlineJs");
|
||||
el.setAttribute("type", "text/javascript");
|
||||
el.textContent = code;
|
||||
document.body?.appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject external JavaScript file
|
||||
export const injectExternalJs = (src) => {
|
||||
const el = document.createElement("script");
|
||||
el.setAttribute("data-source", "KISS-Calendar injectExternalJs");
|
||||
el.setAttribute("type", "text/javascript");
|
||||
el.setAttribute("src", src);
|
||||
document.body?.appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject internal CSS code
|
||||
export const injectInternalCss = (styles) => {
|
||||
const el = document.createElement("style");
|
||||
el.setAttribute("data-source", "KISS-Calendar injectInternalCss");
|
||||
el.textContent = styles;
|
||||
document.head?.appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject external CSS file
|
||||
export const injectExternalCss = (href) => {
|
||||
const el = document.createElement("link");
|
||||
el.setAttribute("data-source", "KISS-Calendar injectExternalCss");
|
||||
el.setAttribute("rel", "stylesheet");
|
||||
el.setAttribute("type", "text/css");
|
||||
el.setAttribute("href", href);
|
||||
document.head?.appendChild(el);
|
||||
};
|
||||
199
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
DEFAULT_INPUT_RULE,
|
||||
DEFAULT_TRANS_APIS,
|
||||
DEFAULT_INPUT_SHORTCUT,
|
||||
OPT_LANGS_LIST,
|
||||
} from "../config";
|
||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { loadingSvg } from "./svg";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
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(node, loadingId) {
|
||||
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
||||
if (div) {
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框翻译
|
||||
*/
|
||||
export default function inputTranslate({
|
||||
inputRule: {
|
||||
transOpen,
|
||||
triggerShortcut,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
triggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = DEFAULT_INPUT_RULE,
|
||||
transApis,
|
||||
}) {
|
||||
if (!transOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||
if (triggerShortcut.length === 0) {
|
||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
triggerCount = 1;
|
||||
}
|
||||
|
||||
stepShortcutRegister(
|
||||
triggerShortcut,
|
||||
async () => {
|
||||
let node = document.activeElement;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.shadowRoot) {
|
||||
node = node.shadowRoot.activeElement;
|
||||
}
|
||||
|
||||
if (!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;
|
||||
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 [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) {
|
||||
kissLog(err, "translate input");
|
||||
} finally {
|
||||
removeLoading(node, loadingId);
|
||||
}
|
||||
},
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
}
|
||||
16
src/libs/interpreter.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Sval from "sval";
|
||||
|
||||
const interpreter = new Sval({
|
||||
// ECMA Version of the code
|
||||
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
|
||||
// or "latest"
|
||||
ecmaVer: "latest",
|
||||
// Code source type
|
||||
// "script" or "module"
|
||||
sourceType: "script",
|
||||
// Whether the code runs in a sandbox
|
||||
sandBox: true,
|
||||
});
|
||||
|
||||
export default interpreter;
|
||||
12
src/libs/log.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 日志函数
|
||||
* @param {*} msg
|
||||
* @param {*} type
|
||||
*/
|
||||
export const kissLog = (msg, type) => {
|
||||
let prefix = `[KISS-Translator]`;
|
||||
if (type) {
|
||||
prefix += `[${type}]`;
|
||||
}
|
||||
console.log(`${prefix} ${msg}`);
|
||||
};
|
||||
1
src/libs/mobile.js
Normal file
@@ -0,0 +1 @@
|
||||
export const isMobile = "ontouchstart" in document.documentElement;
|
||||
38
src/libs/msg.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { browser } from "./browser";
|
||||
|
||||
/**
|
||||
* 获取当前tab信息
|
||||
* @returns
|
||||
*/
|
||||
export const getCurTab = async () => {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
return tab;
|
||||
};
|
||||
|
||||
export const getCurTabId = async () => {
|
||||
const tab = await getCurTab();
|
||||
return tab.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息给background
|
||||
* @param {*} action
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const sendBgMsg = (action, args) =>
|
||||
browser.runtime.sendMessage({ action, args });
|
||||
|
||||
/**
|
||||
* 发送消息给当前页面
|
||||
* @param {*} action
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const sendTabMsg = async (action, args) => {
|
||||
const tabId = await getCurTabId();
|
||||
return browser.tabs.sendMessage(tabId, { action, args });
|
||||
};
|
||||
80
src/libs/pool.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} fn
|
||||
* @param {*} preFn
|
||||
* @param {*} _interval
|
||||
* @param {*} _limit
|
||||
* @returns
|
||||
*/
|
||||
export const taskPool = (
|
||||
fn,
|
||||
preFn,
|
||||
_interval = 100,
|
||||
_limit = 100,
|
||||
_retryInteral = 1000
|
||||
) => {
|
||||
const pool = [];
|
||||
const maxRetry = 2; // 最大重试次数
|
||||
let maxCount = _limit; // 最大数量
|
||||
let curCount = 0; // 当前数量
|
||||
let interval = _interval; // 间隔时间
|
||||
let timer = null;
|
||||
|
||||
const run = async () => {
|
||||
// console.log("timer", timer);
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(run, interval);
|
||||
|
||||
if (curCount < maxCount) {
|
||||
const item = pool.shift();
|
||||
if (item) {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
try {
|
||||
const preArgs = preFn ? await preFn(item.args) : {};
|
||||
const res = await fn({ ...args, ...preArgs });
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
kissLog(err, "task");
|
||||
if (retry < maxRetry) {
|
||||
const retryTimer = setTimeout(() => {
|
||||
clearTimeout(retryTimer);
|
||||
pool.push({ args, resolve, reject, retry: retry + 1 });
|
||||
}, _retryInteral);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} finally {
|
||||
curCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
push: async (args) => {
|
||||
if (!timer) {
|
||||
run();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
pool.push({ args, resolve, reject, retry: 0 });
|
||||
});
|
||||
},
|
||||
update: (_interval = 100, _limit = 100) => {
|
||||
if (_interval >= 0 && _interval <= 5000 && _interval !== interval) {
|
||||
interval = _interval;
|
||||
}
|
||||
if (_limit >= 1 && _limit <= 100 && _limit !== maxCount) {
|
||||
maxCount = _limit;
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
pool.length = 0;
|
||||
curCount = 0;
|
||||
timer && clearTimeout(timer);
|
||||
timer = null;
|
||||
},
|
||||
};
|
||||
};
|
||||
222
src/libs/rules.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import { matchValue, type, isMatch } from "./utils";
|
||||
import {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
OPT_TRANS_ALL,
|
||||
OPT_STYLE_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_TIMING_ALL,
|
||||
GLOBLA_RULE,
|
||||
} from "../config";
|
||||
import { loadOrFetchSubRules } from "./subRules";
|
||||
import { getRulesWithDefault, setRules } from "./storage";
|
||||
import { trySyncRules } from "./sync";
|
||||
import { FIXER_ALL } from "./webfix";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* 根据href匹配规则
|
||||
* @param {*} rules
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = async (
|
||||
href,
|
||||
{ injectRules, subrulesList, owSubrule }
|
||||
) => {
|
||||
const rules = await getRulesWithDefault();
|
||||
if (injectRules) {
|
||||
try {
|
||||
const selectedSub = subrulesList.find((item) => item.selected);
|
||||
if (selectedSub?.url) {
|
||||
const mixRule = {};
|
||||
Object.entries(owSubrule)
|
||||
.filter(([key, val]) => {
|
||||
if (
|
||||
owSubrule.textStyle === REMAIN_KEY &&
|
||||
(key === "bgColor" || key === "textDiyStyle")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return val !== REMAIN_KEY;
|
||||
})
|
||||
.forEach(([key, val]) => {
|
||||
mixRule[key] = val;
|
||||
});
|
||||
|
||||
let subRules = await loadOrFetchSubRules(selectedSub.url);
|
||||
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "load injectRules");
|
||||
}
|
||||
}
|
||||
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
const globalRule = {
|
||||
...GLOBLA_RULE,
|
||||
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
|
||||
};
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
}
|
||||
|
||||
[
|
||||
"selector",
|
||||
"keepSelector",
|
||||
"terms",
|
||||
"selectStyle",
|
||||
"parentStyle",
|
||||
"injectJs",
|
||||
"injectCss",
|
||||
"fixerSelector",
|
||||
"transStartHook",
|
||||
"transEndHook",
|
||||
"transRemoveHook",
|
||||
].forEach((key) => {
|
||||
if (!rule[key]?.trim()) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
[
|
||||
"translator",
|
||||
"fromLang",
|
||||
"toLang",
|
||||
"transOpen",
|
||||
"transOnly",
|
||||
"transTiming",
|
||||
"transTag",
|
||||
"transTitle",
|
||||
"transSelected",
|
||||
"detectRemote",
|
||||
"fixerFunc",
|
||||
].forEach((key) => {
|
||||
if (rule[key] === undefined || rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
||||
rule.skipLangs = globalRule.skipLangs;
|
||||
}
|
||||
if (rule.textStyle === GLOBAL_KEY) {
|
||||
rule.textStyle = globalRule.textStyle;
|
||||
rule.bgColor = globalRule.bgColor;
|
||||
rule.textDiyStyle = globalRule.textDiyStyle;
|
||||
} else {
|
||||
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
|
||||
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
|
||||
}
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查过滤rules
|
||||
* @param {*} rules
|
||||
* @returns
|
||||
*/
|
||||
export const checkRules = (rules) => {
|
||||
if (type(rules) === "string") {
|
||||
rules = JSON.parse(rules);
|
||||
}
|
||||
if (type(rules) !== "array") {
|
||||
throw new Error("data error");
|
||||
}
|
||||
|
||||
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
|
||||
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
|
||||
const patternSet = new Set();
|
||||
rules = rules
|
||||
.filter((rule) => type(rule) === "object")
|
||||
.filter(({ pattern }) => {
|
||||
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
|
||||
return false;
|
||||
}
|
||||
patternSet.add(pattern.trim());
|
||||
return true;
|
||||
})
|
||||
.map(
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
keepSelector,
|
||||
terms,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
injectJs,
|
||||
injectCss,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
transOnly,
|
||||
transTiming,
|
||||
transTag,
|
||||
transTitle,
|
||||
transSelected,
|
||||
detectRemote,
|
||||
skipLangs,
|
||||
fixerSelector,
|
||||
fixerFunc,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
transRemoveHook,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
|
||||
terms: type(terms) === "string" ? terms : "",
|
||||
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
|
||||
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
|
||||
injectJs: type(injectJs) === "string" ? injectJs : "",
|
||||
injectCss: type(injectCss) === "string" ? injectCss : "",
|
||||
bgColor: type(bgColor) === "string" ? bgColor : "",
|
||||
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
|
||||
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
|
||||
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
|
||||
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
|
||||
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
|
||||
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
|
||||
transOnly: matchValue([GLOBAL_KEY, "true", "false"], transOnly),
|
||||
transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
|
||||
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
|
||||
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
|
||||
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
|
||||
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
|
||||
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
|
||||
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
|
||||
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
|
||||
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
|
||||
transRemoveHook:
|
||||
type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||
})
|
||||
);
|
||||
|
||||
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();
|
||||
};
|
||||
112
src/libs/shortcut.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { isSameSet } from "./utils";
|
||||
|
||||
/**
|
||||
* 键盘快捷键监听
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @param {*} timeout
|
||||
* @returns
|
||||
*/
|
||||
export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
||||
const allkeys = new Set();
|
||||
const curkeys = new Set();
|
||||
let timer = null;
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
allkeys.clear();
|
||||
curkeys.clear();
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, timeout);
|
||||
|
||||
if (e.code) {
|
||||
allkeys.add(e.code);
|
||||
curkeys.add(e.code);
|
||||
fn([...curkeys], [...allkeys]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
curkeys.delete(e.code);
|
||||
if (curkeys.size === 0) {
|
||||
fn([...curkeys], [...allkeys]);
|
||||
allkeys.clear();
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener("keydown", handleKeydown, true);
|
||||
target.addEventListener("keyup", handleKeyup, true);
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
target.removeEventListener("keydown", handleKeydown);
|
||||
target.removeEventListener("keyup", handleKeyup);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册键盘快捷键
|
||||
* @param {*} targetKeys
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @returns
|
||||
*/
|
||||
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||
return shortcutListener((curkeys) => {
|
||||
if (
|
||||
targetKeys.length > 0 &&
|
||||
isSameSet(new Set(targetKeys), new Set(curkeys))
|
||||
) {
|
||||
fn();
|
||||
}
|
||||
}, 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);
|
||||
};
|
||||
161
src/libs/storage.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_RULES,
|
||||
STOKEY_WORDS,
|
||||
STOKEY_FAB,
|
||||
STOKEY_SYNC,
|
||||
STOKEY_MSAUTH,
|
||||
STOKEY_BDAUTH,
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
BUILTIN_RULES,
|
||||
} from "../config";
|
||||
import { isExt, isGm } from "./client";
|
||||
import { browser } from "./browser";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
async function set(key, val) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.set({ [key]: val });
|
||||
} else if (isGm) {
|
||||
await (window.KISS_GM || GM).setValue(key, val);
|
||||
} else {
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(key) {
|
||||
if (isExt) {
|
||||
const val = await browser.storage.local.get([key]);
|
||||
return val[key];
|
||||
} else if (isGm) {
|
||||
const val = await (window.KISS_GM || GM).getValue(key);
|
||||
return val;
|
||||
}
|
||||
return window.localStorage.getItem(key);
|
||||
}
|
||||
|
||||
async function del(key) {
|
||||
if (isExt) {
|
||||
await browser.storage.local.remove([key]);
|
||||
} else if (isGm) {
|
||||
await (window.KISS_GM || GM).deleteValue(key);
|
||||
} else {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
async function setObj(key, obj) {
|
||||
await set(key, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
async function trySetObj(key, obj) {
|
||||
if (!(await get(key))) {
|
||||
await setObj(key, obj);
|
||||
}
|
||||
}
|
||||
|
||||
async function getObj(key) {
|
||||
const val = await get(key);
|
||||
return val && JSON.parse(val);
|
||||
}
|
||||
|
||||
async function putObj(key, obj) {
|
||||
const cur = (await getObj(key)) ?? {};
|
||||
await setObj(key, { ...cur, ...obj });
|
||||
}
|
||||
|
||||
/**
|
||||
* 对storage的封装
|
||||
*/
|
||||
export const storage = {
|
||||
get,
|
||||
set,
|
||||
del,
|
||||
setObj,
|
||||
trySetObj,
|
||||
getObj,
|
||||
putObj,
|
||||
// onChanged,
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置信息
|
||||
*/
|
||||
export const getSetting = () => getObj(STOKEY_SETTING);
|
||||
export const getSettingWithDefault = async () => ({
|
||||
...DEFAULT_SETTING,
|
||||
...((await getSetting()) || {}),
|
||||
});
|
||||
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
|
||||
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
||||
|
||||
/**
|
||||
* 规则列表
|
||||
*/
|
||||
export const getRules = () => getObj(STOKEY_RULES);
|
||||
export const getRulesWithDefault = async () =>
|
||||
(await getRules()) || DEFAULT_RULES;
|
||||
export const setRules = (val) => setObj(STOKEY_RULES, val);
|
||||
|
||||
/**
|
||||
* 词汇列表
|
||||
*/
|
||||
export const getWords = () => getObj(STOKEY_WORDS);
|
||||
export const getWordsWithDefault = async () => (await getWords()) || {};
|
||||
export const setWords = (val) => setObj(STOKEY_WORDS, val);
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
*/
|
||||
export const getSubRules = (url) => getObj(STOKEY_RULESCACHE_PREFIX + url);
|
||||
export const getSubRulesWithDefault = async () => (await getSubRules()) || [];
|
||||
export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
|
||||
export const setSubRules = (url, val) =>
|
||||
setObj(STOKEY_RULESCACHE_PREFIX + url, val);
|
||||
|
||||
/**
|
||||
* fab位置
|
||||
*/
|
||||
export const getFab = () => getObj(STOKEY_FAB);
|
||||
export const getFabWithDefault = async () => (await getFab()) || {};
|
||||
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
||||
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
|
||||
|
||||
/**
|
||||
* 数据同步
|
||||
*/
|
||||
export const getSync = () => getObj(STOKEY_SYNC);
|
||||
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
|
||||
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
|
||||
/**
|
||||
* ms auth
|
||||
*/
|
||||
export const getMsauth = () => getObj(STOKEY_MSAUTH);
|
||||
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
|
||||
|
||||
/**
|
||||
* baidu auth
|
||||
*/
|
||||
export const getBdauth = () => getObj(STOKEY_BDAUTH);
|
||||
export const setBdauth = (val) => setObj(STOKEY_BDAUTH, val);
|
||||
|
||||
/**
|
||||
* 存入默认数据
|
||||
*/
|
||||
export const tryInitDefaultData = async () => {
|
||||
try {
|
||||
await trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
|
||||
await trySetObj(STOKEY_RULES, DEFAULT_RULES);
|
||||
await trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
await trySetObj(
|
||||
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
|
||||
BUILTIN_RULES
|
||||
);
|
||||
} catch (err) {
|
||||
kissLog(err, "init default");
|
||||
}
|
||||
};
|
||||
87
src/libs/subRules.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { GLOBAL_KEY } from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
} from "./storage";
|
||||
import { apiFetch } from "../apis";
|
||||
import { checkRules } from "./rules";
|
||||
import { isAllchar } from "./utils";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* 更新缓存同步时间
|
||||
* @param {*} url
|
||||
*/
|
||||
const updateSyncDataCache = async (url) => {
|
||||
const { dataCaches = {} } = await getSyncWithDefault();
|
||||
dataCaches[url] = Date.now();
|
||||
await updateSync({ dataCaches });
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncSubRules = async (url) => {
|
||||
const res = await apiFetch(url);
|
||||
const rules = checkRules(res).filter(
|
||||
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
||||
);
|
||||
if (rules.length > 0) {
|
||||
await setSubRules(url, rules);
|
||||
}
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncAllSubRules = async (subrulesList) => {
|
||||
for (const subrules of subrulesList) {
|
||||
try {
|
||||
await syncSubRules(subrules.url);
|
||||
await updateSyncDataCache(subrules.url);
|
||||
} catch (err) {
|
||||
kissLog(err, `sync subrule error: ${subrules.url}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据时间同步所有订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const trySyncAllSubRules = async ({ subrulesList }) => {
|
||||
try {
|
||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
// 同步订阅规则
|
||||
await syncAllSubRules(subrulesList);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(err, "try sync all subrules");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从缓存或远程加载订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchSubRules = async (url) => {
|
||||
let rules = await getSubRules(url);
|
||||
if (!rules || rules.length === 0) {
|
||||
rules = await syncSubRules(url);
|
||||
await updateSyncDataCache(url);
|
||||
}
|
||||
return rules || [];
|
||||
};
|
||||
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%; max-width: 24; max-height: 24;">
|
||||
<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>
|
||||
`;
|
||||
197
src/libs/sync.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
APP_LCNAME,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
KV_WORDS_KEY,
|
||||
KV_RULES_SHARE_KEY,
|
||||
KV_SALT_SHARE,
|
||||
OPT_SYNCTYPE_WEBDAV,
|
||||
} from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
getSettingWithDefault,
|
||||
getRulesWithDefault,
|
||||
getWordsWithDefault,
|
||||
setSetting,
|
||||
setRules,
|
||||
setWords,
|
||||
} from "./storage";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256, removeEndchar } from "./utils";
|
||||
import { createClient, getPatcher } from "webdav";
|
||||
import { fetchPatcher } from "./fetch";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
getPatcher().patch("request", (opts) => {
|
||||
return fetchPatcher(opts.url, {
|
||||
method: opts.method,
|
||||
headers: opts.headers,
|
||||
body: opts.data,
|
||||
});
|
||||
});
|
||||
|
||||
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
|
||||
const client = createClient(syncUrl, {
|
||||
username: syncUser,
|
||||
password: syncKey,
|
||||
});
|
||||
const pathname = `/${APP_LCNAME}`;
|
||||
const filename = `/${APP_LCNAME}/${data.key}`;
|
||||
|
||||
if ((await client.exists(pathname)) === false) {
|
||||
await client.createDirectory(pathname);
|
||||
}
|
||||
|
||||
const isExist = await client.exists(filename);
|
||||
if (isExist) {
|
||||
const cont = await client.getFileContents(filename, { format: "text" });
|
||||
const webData = JSON.parse(cont);
|
||||
if (webData.updateAt >= data.updateAt) {
|
||||
return webData;
|
||||
}
|
||||
}
|
||||
|
||||
await client.putFileContents(filename, JSON.stringify(data, null, 2));
|
||||
return data;
|
||||
};
|
||||
|
||||
const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
||||
syncUrl = removeEndchar(syncUrl, "/");
|
||||
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||
};
|
||||
|
||||
const syncData = async (key, valueFn) => {
|
||||
const {
|
||||
syncType,
|
||||
syncUrl,
|
||||
syncUser,
|
||||
syncKey,
|
||||
syncMeta = {},
|
||||
} = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||
syncAt === 0 && (updateAt = 0);
|
||||
|
||||
const value = await valueFn();
|
||||
const data = {
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
updateAt,
|
||||
};
|
||||
const args = {
|
||||
syncUrl,
|
||||
syncUser,
|
||||
syncKey,
|
||||
};
|
||||
|
||||
const res =
|
||||
syncType === OPT_SYNCTYPE_WEBDAV
|
||||
? await syncByWebdav(data, args)
|
||||
: await syncByWorker(data, args);
|
||||
|
||||
syncMeta[key] = {
|
||||
updateAt: res.updateAt,
|
||||
syncAt: Date.now(),
|
||||
};
|
||||
await updateSync({ syncMeta });
|
||||
|
||||
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
const syncSetting = async () => {
|
||||
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setSetting(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncSetting = async () => {
|
||||
try {
|
||||
await syncSetting();
|
||||
} catch (err) {
|
||||
kissLog(err, "sync setting");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步规则
|
||||
* @returns
|
||||
*/
|
||||
const syncRules = async () => {
|
||||
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setRules(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncRules = async () => {
|
||||
try {
|
||||
await syncRules();
|
||||
} catch (err) {
|
||||
kissLog(err, "sync user rules");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步词汇
|
||||
* @returns
|
||||
*/
|
||||
const syncWords = async () => {
|
||||
const res = await syncData(KV_WORDS_KEY, getWordsWithDefault);
|
||||
if (res?.isNew) {
|
||||
await setWords(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncWords = async () => {
|
||||
try {
|
||||
await syncWords();
|
||||
} catch (err) {
|
||||
kissLog(err, "sync fav words");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步分享规则
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
const data = {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
value: JSON.stringify(rules, null, 2),
|
||||
updateAt: Date.now(),
|
||||
};
|
||||
const args = {
|
||||
syncUrl,
|
||||
syncKey,
|
||||
};
|
||||
await syncByWorker(data, args);
|
||||
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步个人设置和规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncSettingAndRules = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
await syncWords();
|
||||
};
|
||||
|
||||
export const trySyncSettingAndRules = async () => {
|
||||
await trySyncSetting();
|
||||
await trySyncRules();
|
||||
await trySyncWords();
|
||||
};
|
||||
12
src/libs/touch.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export function touchTapListener(fn, touchsLength) {
|
||||
const handleTouchend = (e) => {
|
||||
if (e.touches.length === touchsLength) {
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleTouchend);
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", handleTouchend);
|
||||
};
|
||||
}
|
||||
563
src/libs/translator.js
Normal file
@@ -0,0 +1,563 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
APP_LCNAME,
|
||||
TRANS_MIN_LENGTH,
|
||||
TRANS_MAX_LENGTH,
|
||||
MSG_TRANS_CURRULE,
|
||||
MSG_INJECT_JS,
|
||||
MSG_INJECT_CSS,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
SHADOW_KEY,
|
||||
OPT_TIMING_PAGESCROLL,
|
||||
OPT_TIMING_PAGEOPEN,
|
||||
OPT_TIMING_MOUSEOVER,
|
||||
DEFAULT_TRANS_APIS,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
} from "../config";
|
||||
import Content from "../views/Content";
|
||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||
import { debounce, genEventName, getHtmlText } from "./utils";
|
||||
import { runFixer } from "./webfix";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { isExt } from "./client";
|
||||
import { injectInlineJs, injectInternalCss } from "./injector";
|
||||
import { kissLog } from "./log";
|
||||
import interpreter from "./interpreter";
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
*/
|
||||
export class Translator {
|
||||
_rule = {};
|
||||
_setting = {};
|
||||
_rootNodes = new Set();
|
||||
_tranNodes = new Map();
|
||||
_skipNodeNames = [
|
||||
APP_LCNAME,
|
||||
"style",
|
||||
"svg",
|
||||
"img",
|
||||
"audio",
|
||||
"video",
|
||||
"textarea",
|
||||
"input",
|
||||
"button",
|
||||
"select",
|
||||
"option",
|
||||
"head",
|
||||
"script",
|
||||
"iframe",
|
||||
];
|
||||
_eventName = genEventName();
|
||||
_mouseoverNode = null;
|
||||
_keepSelector = "";
|
||||
_terms = [];
|
||||
_docTitle = "";
|
||||
|
||||
// 显示
|
||||
_interseObserver = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
observer.unobserve(entry.target);
|
||||
this._render(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
// 变化
|
||||
_mutaObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
!this._skipNodeNames.includes(mutation.target.localName) &&
|
||||
mutation.addedNodes.length > 0
|
||||
) {
|
||||
const nodes = Array.from(mutation.addedNodes).filter((node) => {
|
||||
if (
|
||||
this._skipNodeNames.includes(node.localName) ||
|
||||
node.id === APP_LCNAME
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (nodes.length > 0) {
|
||||
// const rootNode = mutation.target.getRootNode();
|
||||
// todo
|
||||
this._reTranslate();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 插入 shadowroot
|
||||
_overrideAttachShadow = () => {
|
||||
const _this = this;
|
||||
const _attachShadow = HTMLElement.prototype.attachShadow;
|
||||
HTMLElement.prototype.attachShadow = function () {
|
||||
_this._reTranslate();
|
||||
return _attachShadow.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
_updatePool(translator) {
|
||||
if (!translator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||
} = this._setting.transApis[translator] || {};
|
||||
updateFetchPool(fetchInterval, fetchLimit);
|
||||
}
|
||||
|
||||
constructor(rule, setting) {
|
||||
this._overrideAttachShadow();
|
||||
|
||||
this._setting = setting;
|
||||
this._rule = rule;
|
||||
|
||||
this._keepSelector = rule.keepSelector || "";
|
||||
this._terms = (rule.terms || "")
|
||||
.split(/\n|;/)
|
||||
.map((item) => item.split(",").map((item) => item.trim()))
|
||||
.filter(([term]) => Boolean(term));
|
||||
|
||||
this._updatePool(rule.translator);
|
||||
|
||||
if (rule.transOpen === "true") {
|
||||
this._register();
|
||||
}
|
||||
}
|
||||
|
||||
get setting() {
|
||||
return this._setting;
|
||||
}
|
||||
|
||||
get eventName() {
|
||||
return this._eventName;
|
||||
}
|
||||
|
||||
get rule() {
|
||||
// console.log("get rule", this._rule);
|
||||
return this._rule;
|
||||
}
|
||||
|
||||
set rule(rule) {
|
||||
// console.log("set rule", rule);
|
||||
this._rule = rule;
|
||||
|
||||
// 广播消息
|
||||
const eventName = this._eventName;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
detail: {
|
||||
action: MSG_TRANS_CURRULE,
|
||||
args: rule,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateRule = (obj) => {
|
||||
this.rule = { ...this.rule, ...obj };
|
||||
this._updatePool(obj.translator);
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
if (this.rule.transOpen === "true") {
|
||||
this.rule = { ...this.rule, transOpen: "false" };
|
||||
this._unRegister();
|
||||
} else {
|
||||
this.rule = { ...this.rule, transOpen: "true" };
|
||||
this._register();
|
||||
}
|
||||
};
|
||||
|
||||
toggleStyle = () => {
|
||||
const textStyle =
|
||||
this.rule.textStyle === OPT_STYLE_FUZZY
|
||||
? OPT_STYLE_DASHLINE
|
||||
: OPT_STYLE_FUZZY;
|
||||
this.rule = { ...this.rule, textStyle };
|
||||
};
|
||||
|
||||
translateText = async (text) => {
|
||||
const { translator, fromLang, toLang } = this._rule;
|
||||
const apiSetting =
|
||||
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||
const [trText] = await apiTranslate({
|
||||
text,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
return trText;
|
||||
};
|
||||
|
||||
_querySelectorAll = (selector, node) => {
|
||||
try {
|
||||
return Array.from(node.querySelectorAll(selector));
|
||||
} catch (err) {
|
||||
kissLog(selector, "querySelectorAll err");
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
_queryFilter = (selector, rootNode) => {
|
||||
return this._querySelectorAll(selector, rootNode).filter(
|
||||
(node) => this._queryFilter(selector, node).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
_queryShadowNodes = (selector, rootNode) => {
|
||||
this._rootNodes.add(rootNode);
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(rootNode.querySelectorAll("*"))
|
||||
.map((item) => item.shadowRoot)
|
||||
.filter(Boolean)
|
||||
.forEach((item) => {
|
||||
this._queryShadowNodes(selector, item);
|
||||
});
|
||||
};
|
||||
|
||||
_queryNodes = (rootNode = document) => {
|
||||
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
|
||||
// .map((item) => item.shadowRoot)
|
||||
// .filter(Boolean);
|
||||
// const childNodes = childRoots.map((item) => this._queryNodes(item));
|
||||
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
|
||||
// return nodes.concat(childNodes).flat();
|
||||
|
||||
this._rootNodes.add(rootNode);
|
||||
this._rule.selector
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((selector) => {
|
||||
if (selector.includes(SHADOW_KEY)) {
|
||||
const [outSelector, inSelector] = selector
|
||||
.split(SHADOW_KEY)
|
||||
.map((item) => item.trim());
|
||||
if (outSelector && inSelector) {
|
||||
const outNodes = this._querySelectorAll(outSelector, rootNode);
|
||||
outNodes.forEach((outNode) => {
|
||||
if (outNode.shadowRoot) {
|
||||
// this._rootNodes.add(outNode.shadowRoot);
|
||||
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
|
||||
// (item) => {
|
||||
// if (!this._tranNodes.has(item)) {
|
||||
// this._tranNodes.set(item, "");
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
this._queryShadowNodes(inSelector, outNode.shadowRoot);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._queryFilter(selector, rootNode).forEach((item) => {
|
||||
if (!this._tranNodes.has(item)) {
|
||||
this._tranNodes.set(item, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_register = () => {
|
||||
const { fromLang, toLang, injectJs, injectCss, fixerSelector, fixerFunc } =
|
||||
this._rule;
|
||||
if (fromLang === toLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
// webfix
|
||||
if (fixerSelector && fixerFunc !== "-") {
|
||||
runFixer(fixerSelector, fixerFunc);
|
||||
}
|
||||
|
||||
// 注入用户JS/CSS
|
||||
if (isExt) {
|
||||
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
|
||||
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
|
||||
} else {
|
||||
injectJs && injectInlineJs(injectJs);
|
||||
injectCss && injectInternalCss(injectCss);
|
||||
}
|
||||
|
||||
// 搜索节点
|
||||
this._queryNodes();
|
||||
|
||||
this._rootNodes.forEach((node) => {
|
||||
// 监听节点变化;
|
||||
this._mutaObserver.observe(node, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
// characterData: true,
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
!this._rule.transTiming ||
|
||||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
|
||||
) {
|
||||
// 监听节点显示
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
this._interseObserver.observe(node);
|
||||
});
|
||||
} else if (this._rule.transTiming === OPT_TIMING_PAGEOPEN) {
|
||||
// 全文直接翻译
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
this._render(node);
|
||||
});
|
||||
} else {
|
||||
// 监听鼠标悬停
|
||||
window.addEventListener("keydown", this._handleKeydown);
|
||||
this._tranNodes.forEach((_, node) => {
|
||||
node.addEventListener("mouseenter", this._handleMouseover);
|
||||
node.addEventListener("mouseleave", this._handleMouseout);
|
||||
});
|
||||
}
|
||||
|
||||
// 翻译页面标题
|
||||
if (this._rule.transTitle === "true" && !this._docTitle) {
|
||||
const title = document.title;
|
||||
this._docTitle = title;
|
||||
this.translateText(title).then((trText) => {
|
||||
document.title = `${trText} | ${title}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_handleMouseover = (e) => {
|
||||
// console.log("mouseenter", e);
|
||||
if (!this._tranNodes.has(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this._rule.transTiming.slice(3);
|
||||
if (this._rule.transTiming === OPT_TIMING_MOUSEOVER || e[key]) {
|
||||
e.target.removeEventListener("mouseenter", this._handleMouseover);
|
||||
e.target.removeEventListener("mouseleave", this._handleMouseout);
|
||||
this._render(e.target);
|
||||
} else {
|
||||
this._mouseoverNode = e.target;
|
||||
}
|
||||
};
|
||||
|
||||
_handleMouseout = (e) => {
|
||||
// console.log("mouseleave", e);
|
||||
if (!this._tranNodes.has(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._mouseoverNode = null;
|
||||
};
|
||||
|
||||
_handleKeydown = (e) => {
|
||||
// console.log("keydown", e);
|
||||
const key = this._rule.transTiming.slice(3);
|
||||
if (e[key] && this._mouseoverNode) {
|
||||
this._mouseoverNode.removeEventListener(
|
||||
"mouseenter",
|
||||
this._handleMouseover
|
||||
);
|
||||
this._mouseoverNode.removeEventListener(
|
||||
"mouseleave",
|
||||
this._handleMouseout
|
||||
);
|
||||
|
||||
const node = this._mouseoverNode;
|
||||
this._render(node);
|
||||
this._mouseoverNode = null;
|
||||
}
|
||||
};
|
||||
|
||||
_unRegister = () => {
|
||||
// 恢复页面标题
|
||||
if (this._docTitle) {
|
||||
document.title = this._docTitle;
|
||||
this._docTitle = "";
|
||||
}
|
||||
|
||||
// 解除节点变化监听
|
||||
this._mutaObserver.disconnect();
|
||||
|
||||
// 解除节点显示监听
|
||||
// this._interseObserver.disconnect();
|
||||
|
||||
// 移除键盘监听
|
||||
window.removeEventListener("keydown", this._handleKeydown);
|
||||
|
||||
const { transRemoveHook } = this._rule;
|
||||
this._tranNodes.forEach((innerHTML, node) => {
|
||||
if (
|
||||
!this._rule.transTiming ||
|
||||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
|
||||
) {
|
||||
// 解除节点显示监听
|
||||
this._interseObserver.unobserve(node);
|
||||
} else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) {
|
||||
// 移除鼠标悬停监听
|
||||
// node.style.pointerEvents = "none";
|
||||
node.removeEventListener("mouseenter", this._handleMouseover);
|
||||
node.removeEventListener("mouseleave", this._handleMouseout);
|
||||
}
|
||||
|
||||
// 移除/恢复元素
|
||||
if (innerHTML) {
|
||||
if (this._rule.transOnly === "true") {
|
||||
node.innerHTML = innerHTML;
|
||||
} else {
|
||||
node.querySelector(APP_LCNAME)?.remove();
|
||||
}
|
||||
// 钩子函数
|
||||
if (transRemoveHook?.trim()) {
|
||||
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
|
||||
interpreter.exports.transRemoveHook(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 移除用户JS/CSS
|
||||
this._removeInjector();
|
||||
|
||||
// 清空节点集合
|
||||
this._rootNodes.clear();
|
||||
this._tranNodes.clear();
|
||||
|
||||
// 清空任务池
|
||||
clearFetchPool();
|
||||
};
|
||||
|
||||
_removeInjector = () => {
|
||||
document
|
||||
.querySelectorAll(`[data-source^="KISS-Calendar"]`)
|
||||
?.forEach((el) => el.remove());
|
||||
};
|
||||
|
||||
_reTranslate = debounce(() => {
|
||||
if (this._rule.transOpen === "true") {
|
||||
window.removeEventListener("keydown", this._handleKeydown);
|
||||
this._mutaObserver.disconnect();
|
||||
this._interseObserver.disconnect();
|
||||
this._removeInjector();
|
||||
this._register();
|
||||
}
|
||||
}, this._setting.transInterval);
|
||||
|
||||
_invalidLength = (q) =>
|
||||
!q ||
|
||||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
||||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
|
||||
|
||||
_render = (el) => {
|
||||
let traEl = el.querySelector(APP_LCNAME);
|
||||
|
||||
// 已翻译
|
||||
if (traEl) {
|
||||
if (this._rule.transOnly === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const preText = getHtmlText(this._tranNodes.get(el));
|
||||
const curText = getHtmlText(el.innerHTML, APP_LCNAME);
|
||||
if (preText === curText) {
|
||||
return;
|
||||
}
|
||||
|
||||
traEl.remove();
|
||||
}
|
||||
|
||||
// 缓存已翻译元素
|
||||
this._tranNodes.set(el, el.innerHTML);
|
||||
|
||||
let q = el.innerText.trim();
|
||||
const keeps = [];
|
||||
|
||||
// 翻译开始钩子函数
|
||||
const { transStartHook } = this._rule;
|
||||
if (transStartHook?.trim()) {
|
||||
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||
interpreter.exports.transStartHook(el, q);
|
||||
}
|
||||
|
||||
// 保留元素
|
||||
const keepSelector = this._keepSelector.trim();
|
||||
if (keepSelector) {
|
||||
let text = "";
|
||||
el.childNodes.forEach((child) => {
|
||||
if (child.nodeType === 1 && child.matches(keepSelector)) {
|
||||
if (child.nodeName === "IMG") {
|
||||
child.style.cssText += `width: ${child.width}px;`;
|
||||
child.style.cssText += `height: ${child.height}px;`;
|
||||
}
|
||||
text += `[${keeps.length}]`;
|
||||
keeps.push(child.outerHTML);
|
||||
} else if (child.nodeType === 1 || child.nodeType === 3) {
|
||||
text += child.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
if (keeps.length > 0) {
|
||||
// textContent会保留些无用的换行符,严重影响翻译质量
|
||||
if (q.includes("\n")) {
|
||||
q = text;
|
||||
} else {
|
||||
q = text.replaceAll("\n", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 太长或太短
|
||||
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 专业术语
|
||||
if (this._terms.length > 0) {
|
||||
for (const term of this._terms) {
|
||||
const re = new RegExp(term[0], "g");
|
||||
q = q.replace(re, (t) => {
|
||||
const text = `[${keeps.length}]`;
|
||||
keeps.push(`<i class="kiss-trem">${term[1] || t}</i>`);
|
||||
return text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 附加样式
|
||||
const { selectStyle, parentStyle } = this._rule;
|
||||
el.style.cssText += selectStyle;
|
||||
if (el.parentElement) {
|
||||
el.parentElement.style.cssText += parentStyle;
|
||||
}
|
||||
|
||||
// 插入译文节点
|
||||
traEl = document.createElement(APP_LCNAME);
|
||||
traEl.style.visibility = "visible";
|
||||
// if (this._rule.transOnly === "true") {
|
||||
// el.innerHTML = "";
|
||||
// }
|
||||
el.appendChild(traEl);
|
||||
|
||||
// 渲染译文节点
|
||||
const root = createRoot(traEl);
|
||||
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
|
||||
};
|
||||
}
|
||||
269
src/libs/utils.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 限制数字大小
|
||||
* @param {*} num
|
||||
* @param {*} min
|
||||
* @param {*} max
|
||||
* @returns
|
||||
*/
|
||||
export const limitNumber = (num, min = 0, max = 100) => {
|
||||
const number = parseInt(num);
|
||||
if (Number.isNaN(number) || number < min) {
|
||||
return min;
|
||||
} else if (number > max) {
|
||||
return max;
|
||||
}
|
||||
return number;
|
||||
};
|
||||
|
||||
export const limitFloat = (num, min = 0, max = 100) => {
|
||||
const number = parseFloat(num);
|
||||
if (Number.isNaN(number) || number < min) {
|
||||
return min;
|
||||
} else if (number > max) {
|
||||
return max;
|
||||
}
|
||||
return number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 匹配是否为数组中的值
|
||||
* @param {*} arr
|
||||
* @param {*} val
|
||||
* @returns
|
||||
*/
|
||||
export const matchValue = (arr, val) => {
|
||||
if (arr.length === 0 || arr.includes(val)) {
|
||||
return val;
|
||||
}
|
||||
return arr[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* 等待
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
export const sleep = (delay) =>
|
||||
new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {*} func
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
export const debounce = (func, delay = 200) => {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {*} func
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
export const throttle = (func, delay = 200) => {
|
||||
let timer = null;
|
||||
let cache = null;
|
||||
return (...args) => {
|
||||
if (!timer) {
|
||||
func(...args);
|
||||
cache = null;
|
||||
timer = setTimeout(() => {
|
||||
if (cache) {
|
||||
func(...cache);
|
||||
cache = null;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, delay);
|
||||
} else {
|
||||
cache = args;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断字符串全是某个字符
|
||||
* @param {*} s
|
||||
* @param {*} c
|
||||
* @param {*} i
|
||||
* @returns
|
||||
*/
|
||||
export const isAllchar = (s, c, i = 0) => {
|
||||
while (i < s.length) {
|
||||
if (s[i] !== c) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 字符串通配符(*)匹配
|
||||
* @param {*} s
|
||||
* @param {*} p
|
||||
* @returns
|
||||
*/
|
||||
export const isMatch = (s, p) => {
|
||||
if (s.length === 0 || p.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
p = "*" + p + "*";
|
||||
|
||||
let [sIndex, pIndex] = [0, 0];
|
||||
let [sRecord, pRecord] = [-1, -1];
|
||||
while (sIndex < s.length && pRecord < p.length) {
|
||||
if (p[pIndex] === "*") {
|
||||
pIndex++;
|
||||
[sRecord, pRecord] = [sIndex, pIndex];
|
||||
} else if (s[sIndex] === p[pIndex]) {
|
||||
sIndex++;
|
||||
pIndex++;
|
||||
} else if (sRecord + 1 < s.length) {
|
||||
sRecord++;
|
||||
[sIndex, pIndex] = [sRecord, pRecord];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.length === pIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isAllchar(p, "*", pIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* 类型检查
|
||||
* @param {*} o
|
||||
* @returns
|
||||
*/
|
||||
export const type = (o) => {
|
||||
const s = Object.prototype.toString.call(o);
|
||||
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* sha256
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const sha256 = async (text, salt) => {
|
||||
const data = new TextEncoder().encode(text + salt);
|
||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
|
||||
return [...new Uint8Array(digest)]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机事件名称
|
||||
* @returns
|
||||
*/
|
||||
export const genEventName = () => btoa(Math.random()).slice(3, 11);
|
||||
|
||||
/**
|
||||
* 判断两个 Set 是否相同
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @returns
|
||||
*/
|
||||
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) => {
|
||||
switch (sign) {
|
||||
case "//":
|
||||
return str.match(/\/\/([\w-]+)\s+([^]+)/);
|
||||
case "\\":
|
||||
return str.match(/\\([\w-]+)\s+([^]+)/);
|
||||
case "\\\\":
|
||||
return str.match(/\\\\([\w-]+)\s+([^]+)/);
|
||||
case ">":
|
||||
return str.match(/>([\w-]+)\s+([^]+)/);
|
||||
case ">>":
|
||||
return str.match(/>>([\w-]+)\s+([^]+)/);
|
||||
default:
|
||||
}
|
||||
return str.match(/\/([\w-]+)\s+([^]+)/);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否英文单词
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export const isValidWord = (str) => {
|
||||
const regex = /^[a-zA-Z-]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* blob转为base64
|
||||
* @param {*} blob
|
||||
* @returns
|
||||
*/
|
||||
export const blobToBase64 = (blob) => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取html内的文本
|
||||
* @param {*} htmlStr
|
||||
* @param {*} skipTag
|
||||
* @returns
|
||||
*/
|
||||
export const getHtmlText = (htmlStr, skipTag = "") => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlStr, "text/html");
|
||||
|
||||
if (skipTag) {
|
||||
doc.querySelectorAll(skipTag).forEach((el) => el.remove());
|
||||
}
|
||||
|
||||
return doc.body.innerText.trim();
|
||||
};
|
||||
158
src/libs/webfix.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 修复程序类型
|
||||
*/
|
||||
export const FIXER_NONE = "-";
|
||||
export const FIXER_BR = "br";
|
||||
export const FIXER_BN = "bn";
|
||||
export const FIXER_BR_DIV = "brToDiv";
|
||||
export const FIXER_BN_DIV = "bnToDiv";
|
||||
|
||||
export const FIXER_ALL = [
|
||||
FIXER_NONE,
|
||||
FIXER_BR,
|
||||
FIXER_BN,
|
||||
FIXER_BR_DIV,
|
||||
FIXER_BN_DIV,
|
||||
];
|
||||
|
||||
/**
|
||||
* 修复过的标记
|
||||
*/
|
||||
const fixedSign = "kiss-fixed";
|
||||
|
||||
/**
|
||||
* 采用 `br` 换行网站的修复函数
|
||||
* 目标是将 `br` 替换成 `p`
|
||||
* @param {*} node
|
||||
* @returns
|
||||
*/
|
||||
function brFixer(node, tag = "p") {
|
||||
if (node.hasAttribute(fixedSign)) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute(fixedSign, "true");
|
||||
|
||||
const gapTags = ["BR", "WBR"];
|
||||
const newlineTags = [
|
||||
"DIV",
|
||||
"UL",
|
||||
"OL",
|
||||
"LI",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"H4",
|
||||
"H5",
|
||||
"H6",
|
||||
"P",
|
||||
"HR",
|
||||
"PRE",
|
||||
"TABLE",
|
||||
"BLOCKQUOTE",
|
||||
];
|
||||
|
||||
let html = "";
|
||||
node.childNodes.forEach(function (child, index) {
|
||||
if (index === 0) {
|
||||
html += `<${tag} class="kiss-p">`;
|
||||
}
|
||||
|
||||
if (gapTags.indexOf(child.nodeName) !== -1) {
|
||||
html += `</${tag}><${tag} class="kiss-p">`;
|
||||
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
||||
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
|
||||
} else if (child.outerHTML) {
|
||||
html += child.outerHTML;
|
||||
} else if (child.textContent) {
|
||||
html += child.textContent;
|
||||
}
|
||||
|
||||
if (index === node.childNodes.length - 1) {
|
||||
html += `</${tag}>`;
|
||||
}
|
||||
});
|
||||
node.innerHTML = html;
|
||||
}
|
||||
|
||||
function brDivFixer(node) {
|
||||
return brFixer(node, "div");
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标是将 `\n` 替换成 `p`
|
||||
* @param {*} node
|
||||
* @returns
|
||||
*/
|
||||
function bnFixer(node, tag = "p") {
|
||||
if (node.hasAttribute(fixedSign)) {
|
||||
return;
|
||||
}
|
||||
node.setAttribute(fixedSign, "true");
|
||||
node.innerHTML = node.innerHTML
|
||||
.split("\n")
|
||||
.map((item) => `<${tag} class="kiss-p">${item || " "}</${tag}>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function bnDivFixer(node) {
|
||||
return bnFixer(node, "div");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找、监听节点,并执行修复函数
|
||||
* @param {*} selector
|
||||
* @param {*} fixer
|
||||
* @param {*} rootSelector
|
||||
*/
|
||||
function run(selector, fixer, rootSelector) {
|
||||
const mutaObserver = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
mutation.addedNodes.forEach(function (addNode) {
|
||||
if (addNode && addNode.querySelectorAll) {
|
||||
addNode.querySelectorAll(selector).forEach(function (node) {
|
||||
fixer(node);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let rootNodes = [document];
|
||||
if (rootSelector) {
|
||||
rootNodes = document.querySelectorAll(rootSelector);
|
||||
}
|
||||
|
||||
rootNodes.forEach(function (rootNode) {
|
||||
rootNode.querySelectorAll(selector).forEach(function (node) {
|
||||
fixer(node);
|
||||
});
|
||||
mutaObserver.observe(rootNode, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复程序映射
|
||||
*/
|
||||
const fixerMap = {
|
||||
[FIXER_BR]: brFixer,
|
||||
[FIXER_BN]: bnFixer,
|
||||
[FIXER_BR_DIV]: brDivFixer,
|
||||
[FIXER_BN_DIV]: bnDivFixer,
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行fixer
|
||||
* @param {*} param0
|
||||
*/
|
||||
export function runFixer(selector, fixer = "-", rootSelector) {
|
||||
try {
|
||||
if (Object.keys(fixerMap).includes(fixer)) {
|
||||
run(selector, fixerMap[fixer], rootSelector);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[kiss-webfix run]: ${err.message}`);
|
||||
}
|
||||
}
|
||||
10
src/options.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import Options from "./views/Options";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Options />
|
||||
</React.StrictMode>
|
||||
);
|
||||
16
src/popup.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { SettingProvider } from "./hooks/Setting";
|
||||
import ThemeProvider from "./hooks/Theme";
|
||||
import Popup from "./views/Popup";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Popup />
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
28
src/rules.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { BUILTIN_RULES } from "./config/rules";
|
||||
|
||||
(() => {
|
||||
// rules
|
||||
try {
|
||||
const data = JSON.stringify(BUILTIN_RULES, null, 2);
|
||||
const file = path.resolve(
|
||||
__dirname,
|
||||
"../build/web/kiss-translator-rules.json"
|
||||
);
|
||||
fs.writeFileSync(file, data);
|
||||
console.info(`Built-in rules generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// version
|
||||
try {
|
||||
var pjson = require("../package.json");
|
||||
const file = path.resolve(__dirname, "../build/web/version.txt");
|
||||
fs.writeFileSync(file, pjson.version);
|
||||
console.info(`Version file generated: ${file}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
3
src/userscript.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { run } from "./common";
|
||||
|
||||
run(true);
|
||||
180
src/views/Action/Draggable.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { updateFab } from "../../libs/storage";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
const getEdgePosition = ({
|
||||
x: left,
|
||||
y: top,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
hover,
|
||||
}) => {
|
||||
const right = windowWidth - left - width;
|
||||
const bottom = windowHeight - top - height;
|
||||
const min = Math.min(left, top, right, bottom);
|
||||
switch (min) {
|
||||
case right:
|
||||
left = hover ? windowWidth - width : windowWidth - width / 2;
|
||||
break;
|
||||
case left:
|
||||
left = hover ? 0 : -width / 2;
|
||||
break;
|
||||
case bottom:
|
||||
top = hover ? windowHeight - height : windowHeight - height / 2;
|
||||
break;
|
||||
default:
|
||||
top = hover ? 0 : -height / 2;
|
||||
}
|
||||
return { x: left, y: top };
|
||||
};
|
||||
|
||||
function DraggableWrapper({ children, usePaper, ...props }) {
|
||||
if (usePaper) {
|
||||
return (
|
||||
<Paper {...props} elevation={4}>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return <div {...props}>{children}</div>;
|
||||
}
|
||||
|
||||
export default function Draggable({
|
||||
windowSize: { w: windowWidth, h: windowHeight },
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
show,
|
||||
snapEdge,
|
||||
onStart,
|
||||
onMove,
|
||||
handler,
|
||||
children,
|
||||
usePaper,
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [origin, setOrigin] = useState(null);
|
||||
const [position, setPosition] = useState({ x: left, y: top });
|
||||
const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
|
||||
|
||||
const handlePointerDown = (e) => {
|
||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||
onStart && onStart();
|
||||
const { x, y } = position;
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
setOrigin({ x, y, clientX, clientY });
|
||||
};
|
||||
|
||||
const handlePointerMove = (e) => {
|
||||
onMove && onMove();
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
if (origin) {
|
||||
const dx = clientX - origin.clientX;
|
||||
const dy = clientY - origin.clientY;
|
||||
let x = origin.x + dx;
|
||||
let y = origin.y + dy;
|
||||
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);
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e) => {
|
||||
e.stopPropagation();
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e) => {
|
||||
e.stopPropagation();
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapEdge || !!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPosition((pre) => {
|
||||
const edgePosition = getEdgePosition({
|
||||
...pre,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
hover,
|
||||
});
|
||||
setFabPosition(edgePosition);
|
||||
return edgePosition;
|
||||
});
|
||||
}, [
|
||||
origin,
|
||||
hover,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
snapEdge,
|
||||
setFabPosition,
|
||||
]);
|
||||
|
||||
const opacity = useMemo(() => {
|
||||
if (snapEdge) {
|
||||
return hover || origin ? 1 : 0.2;
|
||||
}
|
||||
return origin ? 0.8 : 1;
|
||||
}, [origin, snapEdge, hover]);
|
||||
|
||||
const touchProps = isMobile
|
||||
? {
|
||||
onTouchStart: handlePointerDown,
|
||||
onTouchMove: handlePointerMove,
|
||||
onTouchEnd: handlePointerUp,
|
||||
}
|
||||
: {
|
||||
onPointerDown: handlePointerDown,
|
||||
onPointerMove: handlePointerMove,
|
||||
onPointerUp: handlePointerUp,
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggableWrapper
|
||||
usePaper={usePaper}
|
||||
style={{
|
||||
opacity,
|
||||
position: "fixed",
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
zIndex: 2147483647,
|
||||
display: show ? "block" : "none",
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
touchAction: "none",
|
||||
}}
|
||||
{...touchProps}
|
||||
>
|
||||
{handler}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</DraggableWrapper>
|
||||
);
|
||||
}
|
||||
234
src/views/Action/index.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
import Draggable from "./Draggable";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { SettingProvider } from "../../hooks/Setting";
|
||||
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,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
} from "../../config";
|
||||
import { shortcutRegister } from "../../libs/shortcut";
|
||||
import { sendIframeMsg } from "../../libs/iframe";
|
||||
import { kissLog } from "../../libs/log";
|
||||
import { getI18n } from "../../hooks/I18n";
|
||||
|
||||
export default function Action({ translator, fab }) {
|
||||
const fabWidth = 40;
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const handleWindowResize = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
setWindowSize({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleWindowClick = (e) => {
|
||||
setShowPopup(false);
|
||||
};
|
||||
|
||||
const handleStart = useCallback(() => {
|
||||
setMoved(false);
|
||||
}, []);
|
||||
|
||||
const handleMove = useCallback(() => {
|
||||
setMoved(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
|
||||
const clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
setShowPopup(false);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
|
||||
setShowPopup((pre) => !pre);
|
||||
}),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
clearShortcuts.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册菜单
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
const { contextMenuType, uiLang } = translator.setting;
|
||||
contextMenuType !== 0 &&
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "translate_switch"),
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "toggle_style"),
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "open_menu"),
|
||||
(event) => {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
getI18n(uiLang, "open_setting"),
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
kissLog(err, "registerMenuCommand");
|
||||
}
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [handleWindowResize]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("click", handleWindowClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleWindowClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const popProps = useMemo(() => {
|
||||
const width = Math.min(windowSize.w, 300);
|
||||
const height = Math.min(windowSize.h, 442);
|
||||
const left = (windowSize.w - width) / 2;
|
||||
const top = (windowSize.h - height) / 2;
|
||||
return {
|
||||
windowSize,
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
};
|
||||
}, [windowSize]);
|
||||
|
||||
const fabProps = {
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fab.x ?? -fabWidth,
|
||||
top: fab.y ?? windowSize.h / 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingProvider>
|
||||
<ThemeProvider>
|
||||
<Draggable
|
||||
key="pop"
|
||||
{...popProps}
|
||||
show={showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
usePaper
|
||||
handler={
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header setShowPopup={setShowPopup} />
|
||||
<Divider />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Draggable>
|
||||
<Draggable
|
||||
key="fab"
|
||||
snapEdge
|
||||
{...fabProps}
|
||||
show={fab.isHide ? false : !showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
handler={
|
||||
<Fab
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
if (!moved) {
|
||||
setShowPopup((pre) => !pre);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TranslateIcon
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
}}
|
||||
/>
|
||||
</Fab>
|
||||
}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</SettingProvider>
|
||||
);
|
||||
}
|
||||
14
src/views/Content/LoadingIcon.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { loadingSvg } from "../../libs/svg";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "1.2em",
|
||||
height: "1em",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: loadingSvg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
188
src/views/Content/index.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import LoadingIcon from "./LoadingIcon";
|
||||
import {
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_DIY,
|
||||
DEFAULT_COLOR,
|
||||
MSG_TRANS_CURRULE,
|
||||
} from "../../config";
|
||||
import { useTranslate } from "../../hooks/Translate";
|
||||
import { styled, css } from "@mui/material/styles";
|
||||
import { APP_LCNAME } from "../../config";
|
||||
import interpreter from "../../libs/interpreter";
|
||||
|
||||
const LINE_STYLES = {
|
||||
[OPT_STYLE_LINE]: "solid",
|
||||
[OPT_STYLE_DOTLINE]: "dotted",
|
||||
[OPT_STYLE_DASHLINE]: "dashed",
|
||||
[OPT_STYLE_WAVYLINE]: "wavy",
|
||||
};
|
||||
|
||||
const StyledSpan = styled("span")`
|
||||
${({ textStyle, textDiyStyle, bgColor }) => {
|
||||
switch (textStyle) {
|
||||
case OPT_STYLE_LINE: // 下划线
|
||||
case OPT_STYLE_DOTLINE: // 点状线
|
||||
case OPT_STYLE_DASHLINE: // 虚线
|
||||
case OPT_STYLE_WAVYLINE: // 波浪线
|
||||
return css`
|
||||
opacity: 0.6;
|
||||
-webkit-opacity: 0.6;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: ${LINE_STYLES[textStyle]};
|
||||
text-decoration-color: ${bgColor};
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 0.3em;
|
||||
-webkit-text-decoration-line: underline;
|
||||
-webkit-text-decoration-style: ${LINE_STYLES[textStyle]};
|
||||
-webkit-text-decoration-color: ${bgColor};
|
||||
-webkit-text-decoration-thickness: 2px;
|
||||
-webkit-text-underline-offset: 0.3em;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
}
|
||||
`;
|
||||
case OPT_STYLE_FUZZY: // 模糊
|
||||
return css`
|
||||
filter: blur(0.2em);
|
||||
-webkit-filter: blur(0.2em);
|
||||
&:hover {
|
||||
filter: none;
|
||||
-webkit-filter: none;
|
||||
}
|
||||
`;
|
||||
case OPT_STYLE_HIGHLIGHT: // 高亮
|
||||
return css`
|
||||
color: #fff;
|
||||
background-color: ${bgColor || DEFAULT_COLOR};
|
||||
`;
|
||||
case OPT_STYLE_BLOCKQUOTE: // 引用
|
||||
return css`
|
||||
opacity: 0.6;
|
||||
-webkit-opacity: 0.6;
|
||||
display: block;
|
||||
padding: 0 0.75em;
|
||||
border-left: 0.25em solid ${bgColor || DEFAULT_COLOR};
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
}
|
||||
`;
|
||||
case OPT_STYLE_DIY: // 自定义
|
||||
return textDiyStyle;
|
||||
default:
|
||||
return ``;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
export default function Content({ q, keeps, translator, $el }) {
|
||||
const [rule, setRule] = useState(translator.rule);
|
||||
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
|
||||
const {
|
||||
transOpen,
|
||||
textStyle,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
transOnly,
|
||||
transTag,
|
||||
transEndHook,
|
||||
} = rule;
|
||||
|
||||
const { newlineLength } = translator.setting;
|
||||
|
||||
const handleKissEvent = (e) => {
|
||||
const { action, args } = e.detail;
|
||||
switch (action) {
|
||||
case MSG_TRANS_CURRULE:
|
||||
setRule(args);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(translator.eventName, handleKissEvent);
|
||||
return () => {
|
||||
window.removeEventListener(translator.eventName, handleKissEvent);
|
||||
};
|
||||
}, [translator.eventName]);
|
||||
|
||||
useEffect(() => {
|
||||
// 运行钩子函数
|
||||
if (text && transEndHook?.trim()) {
|
||||
interpreter.run(`exports.transEndHook = ${transEndHook}`);
|
||||
interpreter.exports.transEndHook($el, q, text, keeps);
|
||||
}
|
||||
}, [$el, q, text, keeps, transEndHook]);
|
||||
|
||||
const gap = useMemo(() => {
|
||||
if (transOnly === "true") {
|
||||
return "";
|
||||
}
|
||||
return q.length >= newlineLength ? <br /> : " ";
|
||||
}, [q, transOnly, newlineLength]);
|
||||
|
||||
const styles = useMemo(
|
||||
() => ({
|
||||
textStyle,
|
||||
textDiyStyle,
|
||||
bgColor,
|
||||
as: transTag,
|
||||
}),
|
||||
[textStyle, textDiyStyle, bgColor, transTag]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
{gap}
|
||||
<LoadingIcon />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!text || sameLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
transOnly === "true" &&
|
||||
transOpen === "true" &&
|
||||
$el.querySelector(APP_LCNAME)
|
||||
) {
|
||||
Array.from($el.childNodes).forEach((el) => {
|
||||
if (el.localName !== APP_LCNAME) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (keeps.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{gap}
|
||||
<StyledSpan
|
||||
{...styles}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{gap}
|
||||
<StyledSpan {...styles}>{text}</StyledSpan>
|
||||
</>
|
||||
);
|
||||
}
|
||||
20
src/views/Options/About.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useI18n, useI18nMd } from "../../hooks/I18n";
|
||||
|
||||
export default function About() {
|
||||
const i18n = useI18n();
|
||||
const [data, loading, error] = useI18nMd("about_md");
|
||||
return (
|
||||
<Box>
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
<ReactMarkdown children={error ? i18n("about_md_local") : data} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
474
src/views/Options/Apis.js
Normal file
@@ -0,0 +1,474 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
URL_NIUTRANS_REG,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
} from "../../config";
|
||||
import { useState } from "react";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { useApi } from "../../hooks/Api";
|
||||
import { apiTranslate } from "../../apis";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import { limitNumber, limitFloat } from "../../libs/utils";
|
||||
|
||||
function TestButton({ translator, api }) {
|
||||
const i18n = useI18n();
|
||||
const alert = useAlert();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleApiTest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [text] = await apiTranslate({
|
||||
translator,
|
||||
text: "hello world",
|
||||
fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
apiSetting: api,
|
||||
useCache: false,
|
||||
});
|
||||
if (!text) {
|
||||
throw new Error("empty result");
|
||||
}
|
||||
alert.success(i18n("test_success"));
|
||||
} catch (err) {
|
||||
// alert.error(`${i18n("test_failed")}: ${err.message}`);
|
||||
let msg = err.message;
|
||||
try {
|
||||
msg = JSON.stringify(JSON.parse(err.message), null, 2);
|
||||
} catch (err) {
|
||||
// skip
|
||||
}
|
||||
alert.error(
|
||||
<>
|
||||
<div>{i18n("test_failed")}</div>
|
||||
{msg === err.message ? (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleApiTest}
|
||||
loading={loading}
|
||||
>
|
||||
{i18n("click_test")}
|
||||
</LoadingButton>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiFields({ translator, api, updateApi, resetApi }) {
|
||||
const i18n = useI18n();
|
||||
const {
|
||||
url = "",
|
||||
key = "",
|
||||
model = "",
|
||||
systemPrompt = "",
|
||||
userPrompt = "",
|
||||
think = false,
|
||||
thinkIgnore = "",
|
||||
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||
dictNo = "",
|
||||
memoryNo = "",
|
||||
reqHook = "",
|
||||
resHook = "",
|
||||
temperature = 0,
|
||||
maxTokens = 256,
|
||||
apiName = "",
|
||||
isDisabled = false,
|
||||
} = api;
|
||||
|
||||
const handleChange = (e) => {
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "fetchLimit":
|
||||
value = limitNumber(value, 1, 100);
|
||||
break;
|
||||
case "fetchInterval":
|
||||
value = limitNumber(value, 0, 5000);
|
||||
break;
|
||||
case "httpTimeout":
|
||||
value = limitNumber(value, 5000, 30000);
|
||||
break;
|
||||
case "temperature":
|
||||
value = limitFloat(value, 0, 2);
|
||||
break;
|
||||
case "maxTokens":
|
||||
value = limitNumber(value, 0, 2 ** 15);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateApi({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const builtinTranslators = [
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
];
|
||||
|
||||
const mulkeysTranslators = [
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_OPENAI_2,
|
||||
OPT_TRANS_OPENAI_3,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OLLAMA_2,
|
||||
OPT_TRANS_OLLAMA_3,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_TRANS_CUSTOMIZE_2,
|
||||
OPT_TRANS_CUSTOMIZE_3,
|
||||
OPT_TRANS_CUSTOMIZE_4,
|
||||
OPT_TRANS_CUSTOMIZE_5,
|
||||
];
|
||||
|
||||
const keyHelper =
|
||||
translator === OPT_TRANS_NIUTRANS ? (
|
||||
<>
|
||||
{i18n("mulkeys_help")}
|
||||
<Link href={URL_NIUTRANS_REG} target="_blank">
|
||||
{i18n("reg_niutrans")}
|
||||
</Link>
|
||||
</>
|
||||
) : mulkeysTranslators.includes(translator) ? (
|
||||
i18n("mulkeys_help")
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("api_name")}
|
||||
name="apiName"
|
||||
value={apiName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{!builtinTranslators.includes(translator) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"URL"}
|
||||
name="url"
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
multiline={translator === OPT_TRANS_DEEPLX}
|
||||
maxRows={10}
|
||||
helperText={
|
||||
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"KEY"}
|
||||
name="key"
|
||||
value={key}
|
||||
onChange={handleChange}
|
||||
multiline={mulkeysTranslators.includes(translator)}
|
||||
maxRows={10}
|
||||
helperText={keyHelper}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||
translator.startsWith(OPT_TRANS_OLLAMA) ||
|
||||
translator === OPT_TRANS_CLAUDE ||
|
||||
translator.startsWith(OPT_TRANS_GEMINI)) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"MODEL"}
|
||||
name="model"
|
||||
value={model}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"SYSTEM PROMPT"}
|
||||
name="systemPrompt"
|
||||
value={systemPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"USER PROMPT"}
|
||||
name="userPrompt"
|
||||
value={userPrompt}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{translator.startsWith(OPT_TRANS_OLLAMA) && (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="think"
|
||||
value={think}
|
||||
label={i18n("if_think")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={false}>{i18n("nothink")}</MenuItem>
|
||||
<MenuItem value={true}>{i18n("think")}</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("think_ignore")}
|
||||
name="thinkIgnore"
|
||||
value={thinkIgnore}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||
translator === OPT_TRANS_CLAUDE ||
|
||||
translator === OPT_TRANS_GEMINI ||
|
||||
translator === OPT_TRANS_GEMINI_2) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Temperature"}
|
||||
type="number"
|
||||
name="temperature"
|
||||
value={temperature}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Max Tokens"}
|
||||
type="number"
|
||||
name="maxTokens"
|
||||
value={maxTokens}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{translator === OPT_TRANS_NIUTRANS && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"DictNo"}
|
||||
name="dictNo"
|
||||
value={dictNo}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"MemoryNo"}
|
||||
name="memoryNo"
|
||||
value={memoryNo}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Request Hook"}
|
||||
name="reqHook"
|
||||
value={reqHook}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label={"Response Hook"}
|
||||
name="resHook"
|
||||
value={resHook}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={10}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("fetch_limit")}
|
||||
type="number"
|
||||
name="fetchLimit"
|
||||
value={fetchLimit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("fetch_interval")}
|
||||
type="number"
|
||||
name="fetchInterval"
|
||||
value={fetchInterval}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
label={i18n("http_timeout")}
|
||||
type="number"
|
||||
name="httpTimeout"
|
||||
defaultValue={httpTimeout}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="isDisabled"
|
||||
checked={isDisabled}
|
||||
onChange={() => {
|
||||
updateApi({ isDisabled: !isDisabled });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("is_disabled")}
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TestButton translator={translator} api={api} />
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
resetApi();
|
||||
}}
|
||||
>
|
||||
{i18n("restore_default")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
||||
<pre>{i18n("custom_api_help")}</pre>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiAccordion({ translator }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { api, updateApi, resetApi } = useApi(translator);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setExpanded((pre) => !pre);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>
|
||||
{api.apiName ? `${translator} (${api.apiName})` : translator}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{expanded && (
|
||||
<ApiFields
|
||||
translator={translator}
|
||||
api={api}
|
||||
updateApi={updateApi}
|
||||
resetApi={resetApi}
|
||||
/>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Apis() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="info">{i18n("about_api")}</Alert>
|
||||
|
||||
<Box>
|
||||
{OPT_TRANS_ALL.map((translator) => (
|
||||
<ApiAccordion key={translator} translator={translator} />
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||