This commit is contained in:
commit
564192f678
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [windingwind]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: windingwind
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ["https://paypal.me/windingwind?country.x=C2&locale.x=zh_XC", ]
|
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[Bug]Complete the Title Here"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Before Issue/提问之前**
|
||||||
|
1. See #6 and search the [Issues Page](https://github.com/windingwind/zotero-pdf-translate/issues) to make sure this is not a duplicated issue;
|
||||||
|
2. If this is a bug with Translate, i.e. no result or error, make sure you can access the translate service and the secret is currect;
|
||||||
|
|
||||||
|
**Issues not in the template format will be ignored/不按模板格式或不完整填写模板内容的提问将不会被回复。如果你看不懂英文可以用中文提问,但请有礼貌地详细描述你的问题。如果不详细说明通过什么步骤可以复现问题,具体表现是什么,你正在使用哪个版本的插件,我没办法帮到你。不要浪费大家的时间!!!**
|
||||||
|
|
||||||
|
**Edit the template below and delete the section above/必须准确详细地填写编辑以下提问模版,并删除上面的内容,否则不会被回复**
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
- OS: Windows/macOS/Linux
|
||||||
|
- Zotero Version(Help->About Zotero)
|
||||||
|
- Addon Version(Tools->Add-ons)
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature]Complete the Title Here"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Before Issue/提问之前**
|
||||||
|
1. See #6 and search the [Issues Page](https://github.com/windingwind/zotero-pdf-translate/issues) to make sure this is not a duplicated issue;
|
||||||
|
|
||||||
|
**Issues not in the template format will be ignored/不按模板格式或不完整填写模板内容的提问将不会被回复。如果你看不懂英文可以用中文提问,但请有礼貌地详细描述你的问题,不要浪费大家的时间!!!**
|
||||||
|
|
||||||
|
**Edit the template below and delete the section above/必须准确详细地填写编辑以下提问模版,并删除上面的内容,否则不会被回复**
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
23
.github/workflows/issuebot.yml
vendored
Normal file
23
.github/workflows/issuebot.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Close inactive issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
days-before-issue-stale: 30
|
||||||
|
days-before-issue-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||||
|
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||||
|
exempt-issue-labels: "help wanted"
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
**/builds
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
zotero-cmd.json
|
13
.release-it.json
Normal file
13
.release-it.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"npm": {
|
||||||
|
"publish": false
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"assets": ["builds/*.xpi"]
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"after:bump": "npm run build",
|
||||||
|
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||||
|
}
|
||||||
|
}
|
29
.vscode/launch.json
vendored
Normal file
29
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Restart-Z6",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "restart-dev-z6"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Restart-Z7",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "restart-dev-z7"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Restart in Prod Mode",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "restart-prod"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
45
.vscode/toolkit.code-snippets
vendored
Normal file
45
.vscode/toolkit.code-snippets
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"appendElement - full": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "appendElement",
|
||||||
|
"body": [
|
||||||
|
"appendElement({",
|
||||||
|
"\ttag: '${1:div}',",
|
||||||
|
"\tid: '${2:id}',",
|
||||||
|
"\tnamespace: '${3:html}',",
|
||||||
|
"\tclassList: ['${4:class}'],",
|
||||||
|
"\tstyles: {${5:style}: '$6'},",
|
||||||
|
"\tproperties: {},",
|
||||||
|
"\tattributes: {},",
|
||||||
|
"\t[{ '${7:onload}', (e: Event) => $8, ${9:false} }],",
|
||||||
|
"\tcheckExistanceParent: ${10:HTMLElement},",
|
||||||
|
"\tignoreIfExists: ${11:true},",
|
||||||
|
"\tskipIfExists: ${12:true},",
|
||||||
|
"\tremoveIfExists: ${13:true},",
|
||||||
|
"\tcustomCheck: (doc: Document, options: ElementOptions) => ${14:true},",
|
||||||
|
"\tchildren: [$15]",
|
||||||
|
"}, ${16:container});"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"appendElement - minimum": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "appendElement",
|
||||||
|
"body": "appendElement({ tag: '$1' }, $2);"
|
||||||
|
},
|
||||||
|
"register Notifier": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "registerObserver",
|
||||||
|
"body": [
|
||||||
|
"registerObserver({",
|
||||||
|
"\t notify: (",
|
||||||
|
"\t\tevent: _ZoteroTypes.Notifier.Event,",
|
||||||
|
"\t\ttype: _ZoteroTypes.Notifier.Type,",
|
||||||
|
"\t\tids: string[],",
|
||||||
|
"\t\textraData: _ZoteroTypes.anyObj",
|
||||||
|
"\t) => {",
|
||||||
|
"\t\t$0",
|
||||||
|
"\t}",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
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 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 work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
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 AGPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
239
README.md
Normal file
239
README.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# Zotero PDF Translate
|
||||||
|
|
||||||
|
[](https://github.com/windingwind/zotero-plugin-template)
|
||||||
|
|
||||||
|
This is an add-on for [Zotero](https://www.zotero.org/)'s built-in PDF reader.
|
||||||
|
Translate PDFs, annotations, notes, and item titles automatically.
|
||||||
|
|
||||||
|
[中文文档](https://zotero.yuque.com/books/share/4443494c-c698-4e08-9d1e-ed253390346d)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### From local file
|
||||||
|
|
||||||
|
- Download the latest release (.xpi file) from the [Latest Release Page](https://github.com/windingwind/zotero-pdf-translate/releases/latest)
|
||||||
|
_Note_ If you're using Firefox as your browser, right-click the `.xpi` and select "Save As.."
|
||||||
|
- In Zotero click `Tools` in the top menu bar and then click `Addons`
|
||||||
|
- Go to the Extensions page and then click the gear icon in the top right.
|
||||||
|
- Select `Install Add-on from file`.
|
||||||
|
- Browse to where you downloaded the `.xpi` file and select it.
|
||||||
|
- Restart Zotero, by clicking `restart now` in the extensions list where the
|
||||||
|
Zotero PDF Translate plugin is now listed.
|
||||||
|
|
||||||
|
### From remote link
|
||||||
|
|
||||||
|
- In Zotero click `Tools` in the top menu bar and then click `Addons`.
|
||||||
|
- Drag [Latest Release](https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi) and drop it in the Zotero UI.
|
||||||
|
- Click `install now`.
|
||||||
|
- Restart Zotero, by clicking `restart now` in the extensions list where the
|
||||||
|
Zotero PDF Translate plugin is now listed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once you have the plugin installed simply, open any PDF in your collections.
|
||||||
|
|
||||||
|
- Select some text, the translations are shown on the popup and the right sidebar(v0.2.0); Hold `Alt/Option` to concat selections.
|
||||||
|

|
||||||
|
|
||||||
|
- Highlight some text, the translations are added to the annotation comment(v0.3.0); Modify & retranslate the annotation text in the sidebar and click the `Update Annotation` to modify the annotation text and translation(v0.6.6);
|
||||||
|
- Add selected text along with translation to note(v0.4.0); _Only works when a note editor is active._
|
||||||
|

|
||||||
|
- Translate item titles with right-click menu or shortcut `Ctrl+T`(v0.6.0).
|
||||||
|
- Translate item abstract with right-click menu(v0.8.0). Thanks @iShareStuff
|
||||||
|
- Standalone translation window available(v0.7.0). View & compare translations from multiply engines in one window!
|
||||||
|

|
||||||
|
- Dictionary for single word translation(v0.7.1).
|
||||||
|
- SentenceBySentence Translation(v1.1.0). After a translation, press `shift`+`P` and select `Translate Sentences`. _Only for en2zh and en2en now_. Thanks @MuiseDestiny
|
||||||
|
|
||||||
|
### Q&A
|
||||||
|
|
||||||
|
**Q** I want to translate manually.
|
||||||
|
**A** Go to `Edit->Preferences->PDF Translate->General`, uncheck the `Automatic Translation`. Click the `translate` button on the popup or sidebar to translate.
|
||||||
|
|
||||||
|
**Q** I want a translate shortcut.
|
||||||
|
**A**
|
||||||
|
Press shortcut `Ctrl+T` after you selected some text. If you are in the collection view, the titles' translation will show/hide.
|
||||||
|
|
||||||
|
**Q** I want to concat different seletions and translate them together.
|
||||||
|
**A** Press `Alt/Option` when selecting text in PDF.
|
||||||
|
|
||||||
|
**Q** Not the language I want.
|
||||||
|
**A** The default target language is the same as your Zotero language. Go to `Edit->Preferences->PDF Translate->General` and change the language settings.
|
||||||
|
|
||||||
|
**Q** Translation not correct or report an error.
|
||||||
|
**A** See [Language Settings](#general-language-settings) and #6. Make sure you use the right secret.
|
||||||
|
|
||||||
|
**Q** I want to change the font size.
|
||||||
|
**A** Go to `Edit->Preferences->PDF Translate->Advanced` and set the font size.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
- Enable Translation, default `true`
|
||||||
|
- Automatic Translation, default `true`
|
||||||
|
- Enable Dictionary: single word will be translated using dictionary-engine instead of translate engine, default `true`
|
||||||
|
- Enable Popup: Show results in a right-click popup or only in the sidebar, default `true`
|
||||||
|
- Automatic Annotation Translation: Save annotation's translation as comment, default `true`
|
||||||
|
- Show 'Add to Note(With Translation)' in Popup: default `true`
|
||||||
|
> Unvisible if no active note editor opened.
|
||||||
|
- Replace Source Text: Use translation to replace the source text when adding to note, default `false`
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
The default engine is Google Translate. Currently, we support:
|
||||||
|
| Translate Engine | Require Secret | Supported Languages |
|
||||||
|
| ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| Google Translate | No | [100+](https://translate.google.com/about/languages/) |
|
||||||
|
| Google Translate(API) | No | Use `translate.googleapis.com` |
|
||||||
|
| CNKI | No | https://dict.cnki.net |
|
||||||
|
| Youdao Translate | No | [100+?](https://ai.youdao.com/DOCSIRMA/html/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E7%BF%BB%E8%AF%91/API%E6%96%87%E6%A1%A3/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1-API%E6%96%87%E6%A1%A3.html) |
|
||||||
|
| Youdao Zhiyun | Yes | [100+](https://ai.youdao.com/DOCSIRMA/html/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E7%BF%BB%E8%AF%91/API%E6%96%87%E6%A1%A3/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1-API%E6%96%87%E6%A1%A3.html) |
|
||||||
|
| Niu Translate(Trial) | No | [100+](https://niutrans.com/documents/contents/trans_text#accessMode) **UNSTABLE** |
|
||||||
|
| Niu Translate | Yes | [100+](https://niutrans.com/documents/contents/trans_text#accessMode) |
|
||||||
|
| Microsoft Translate | Yes(free 2M) | [200+](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/language-support) |
|
||||||
|
| LingoCloud(Caiyun) Translate | Yes | [zh, en, ja, es, fr, ru](https://open.caiyunapp.com/LingoCloud_API_in_5_minutes) |
|
||||||
|
| DeepL Translate | Yes(free 500k) | [100+](https://www.deepl.com/pro?cta=header-prices/#developer) |
|
||||||
|
| Baidu Translate | Yes(free-QPS1/free-2M) | [200+](https://fanyi-api.baidu.com/product/11) |
|
||||||
|
| Baidu Field | Yes(free-QPS1/free-2M) | [en-zh](https://fanyi-api.baidu.com/product/12) |
|
||||||
|
| Tencent Translate | Yes(QPS5, free-5M) | [15](https://cloud.tencent.com/document/product/551/7372) |
|
||||||
|
| GPT(OpenAI) | Yes(free-$18) | [Based on the gpt-3.5-turbo model](https://openai.com/pricing#chat) |
|
||||||
|
|
||||||
|
> If the engine you want is not yet supported, please post an issue.
|
||||||
|
|
||||||
|
**Microsoft Translate**
|
||||||
|
Apply [here](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/quickstart-translator?tabs=csharp). Copy your secret and paste it into the settings.
|
||||||
|
The secret format is `MY_SECRET`.
|
||||||
|
|
||||||
|
> See [this issue](https://github.com/windingwind/zotero-pdf-translate/issues/3#issuecomment-1064688597) for detailed steps to set up the Microsoft Translate.
|
||||||
|
|
||||||
|
**DeepL Translate**
|
||||||
|
Apply [here](https://www.deepl.com/pro?cta=header-prices/#developer).
|
||||||
|
|
||||||
|
**Youdao Zhiyun Translate 有道智云**
|
||||||
|
Apply [here](https://ai.youdao.com/login.s).
|
||||||
|
The secret format is `MY_APPID#MY_SECRET#MY_VOCABID(optional)`.
|
||||||
|
|
||||||
|
> About `VOCABID`
|
||||||
|
> 登录控制台,选择文本翻译服务,点击右侧的术语表,选择新建,填写表名称和语言方向,添加需要的术语表,然后获取对应词表 id 即可。
|
||||||
|
|
||||||
|
> [Official Document](https://ai.youdao.com/DOCSIRMA/html/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E7%BF%BB%E8%AF%91/API%E6%96%87%E6%A1%A3/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1-API%E6%96%87%E6%A1%A3.html)
|
||||||
|
|
||||||
|
**Niu Translate**
|
||||||
|
Apply [here](https://niutrans.com/NiuTransAuthCenter/login).
|
||||||
|
The secret format is `MY_APIKEY#dictNo(optional)#memoryNo(optional)`.
|
||||||
|
|
||||||
|
> [Chinese Document](https://doc.tern.1c7.me/zh/folder/setting/#%E5%B0%8F%E7%89%9B)
|
||||||
|
|
||||||
|
**Baidu Translate**
|
||||||
|
Apply [here](https://fanyi-api.baidu.com/product/11).
|
||||||
|
The secret format is `MY_APPID#MY_KEY#ACTION(optional, see https://api.fanyi.baidu.com/doc/21, default 0)`(split with '#').
|
||||||
|
|
||||||
|
**Baidu Field Translate 百度垂直领域翻译**
|
||||||
|
Apply [here](https://fanyi-api.baidu.com/product/12).
|
||||||
|
The secret format is `MY_APPID#MY_KEY#DOMAIN_CODE`(split with '#').
|
||||||
|
|
||||||
|
| Domain Code | 领域 | 语言方向 |
|
||||||
|
| ----------- | ------------ | ----------- |
|
||||||
|
| electronics | 电子科技领域 | 中文-->英语 |
|
||||||
|
| finance | 金融财经领域 | 中文-->英语 |
|
||||||
|
| finance | 金融财经领域 | 英语-->中文 |
|
||||||
|
| mechanics | 水利机械领域 | 中文-->英语 |
|
||||||
|
| medicine | 生物医药领域 | 中文-->英语 |
|
||||||
|
| medicine | 生物医药领域 | 英语-->中文 |
|
||||||
|
| novel | 网络文学领域 | 中文-->英语 |
|
||||||
|
|
||||||
|
> [Chinese Document](https://doc.tern.1c7.me/zh/folder/setting/#%E8%85%BE%E8%AE%AF%E4%BA%91)
|
||||||
|
|
||||||
|
**Tencent Translate**
|
||||||
|
Apply [here](https://cloud.tencent.com/product/tmt).
|
||||||
|
The secret format is `secretId#SecretKey#Region(optional, default ap-shanghai)#ProjectId(optional, default 0)`(split with '#').
|
||||||
|
|
||||||
|
> [Chinese Document](https://doc.tern.1c7.me/zh/folder/setting/#%E8%85%BE%E8%AE%AF%E4%BA%91)
|
||||||
|
|
||||||
|
**OpenL Translate**
|
||||||
|
Apply [here](https://my.openl.club/).
|
||||||
|
The secret format is `service1,service2,...#apikey`(split with '#'; split service codes with ',').
|
||||||
|
|
||||||
|
Supported service codes are: `deepl,youdao,tencent,aliyun,baidu,caiyun,wechat,sogou,azure,ibm,aws,google`, See [Service Code](https://docs.openl.club/#/API/format?id=%e7%bf%bb%e8%af%91%e6%9c%8d%e5%8a%a1%e4%bb%a3%e7%a0%81%e5%90%8d)
|
||||||
|
|
||||||
|
> [Chinese Document](https://docs.openl.club/#/)
|
||||||
|
|
||||||
|
**GPT**
|
||||||
|
Apply [here](https://platform.openai.com/signup).
|
||||||
|
The secret format is `sk-#SecretKey`(split with '#').
|
||||||
|
|
||||||
|
> [Chinese Document](https://gist.github.com/GrayXu/f1b72353b4b0493d51d47f0f7498b67b)
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
|
||||||
|
- `Font Size`: The font size of result text, default `12`
|
||||||
|
- `Line Height`: The line height of result text, default `1.5`
|
||||||
|
- `SideBar: Show xxx`: Show or hide sidebar elements, default `true`
|
||||||
|
- `SideBar: Reverse Raw/Result`: Reverse the order of Raw/Result in the sidebar if `true`, default `false`
|
||||||
|
- `Popup: Remember Size`: Remember size of popup if `true`, else automatically adjust the size, default `false`
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
|
||||||
|
- Disable Automatic Translation when File Language is(split with ','): If you want to disable automatic translation in `zh` and `ja` files, set `zh,ja`.
|
||||||
|
|
||||||
|
## Development & Contributing
|
||||||
|
|
||||||
|
This addon is built based on the [Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template). See the setup and debug details there.
|
||||||
|
|
||||||
|
To startup, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/windingwind/zotero-pdf-translate.git
|
||||||
|
cd zotero-pdf-translate
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin is built to `./builds/*.xpi`.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
**Add new translate service**
|
||||||
|
|
||||||
|
1. Add service config to `src/utils/config.ts` > `SERVICES`;
|
||||||
|
2. Add translation task processor under `src/modules/services/${serviceId}.ts` with the same format with other services. The export function set the translation result to `data.result` if runs successfully and throw an error if fails;
|
||||||
|
3. Import the task processor function in `src/modules/services.ts`.
|
||||||
|
4. Add locale string `service.${serviceId}` in `addon/chrome/locale/${lang}/addon.properties`.
|
||||||
|
5. Build and test.
|
||||||
|
|
||||||
|
**Extra options for translate service**
|
||||||
|
|
||||||
|
If the service requires extra options, the minimal implement would be putting them in the `secret` input in the prefs window, like the existing services does.
|
||||||
|
|
||||||
|
If there are complex options, please bind a callback in `src/utils/translate.ts > secretStatusButtonData` which create a highly customizable dialog window with `ztoolkit.Dialog`. See the example of NiuTrans login here: https://github.com/windingwind/zotero-pdf-translate/blob/main/src/utils/niuTransLogin.ts
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Use this code under AGPL. No warranties are provided. Keep the laws of your locality in mind!
|
||||||
|
|
||||||
|
## My Other Zotero Addons
|
||||||
|
|
||||||
|
- [zotero-better-notes](https://github.com/windingwind/zotero-better-notes): Everything about note management. All in Zotero.
|
||||||
|
- [zotero-pdf-preview](https://github.com/windingwind/zotero-tag): PDF preview for Zotero
|
||||||
|
- [zotero-tag](https://github.com/windingwind/zotero-tag): Automatically tag items/Batch tagging
|
||||||
|
|
||||||
|
## Sponsor Me
|
||||||
|
|
||||||
|
I'm windingwind, an active Zotero(https://www.zotero.org) plugin developer. Devoting to making reading papers easier.
|
||||||
|
|
||||||
|
Sponsor me to buy a cup of coffee. I spend more than 24 hours every week coding, debugging, and replying to issues in my plugin repositories. The plugins are open-source and totally free.
|
||||||
|
|
||||||
|
If you sponsor more than $10 a month, you can list your name/logo here and have priority for feature requests/bug fixes!
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Thanks
|
||||||
|
[peachgirl100](https://github.com/peachgirl100)
|
||||||
|
and other anonymous sponsors!
|
||||||
|
|
||||||
|
If you want to leave your name here, please email me or leave a message with the donation.
|
148
addon/bootstrap.js
vendored
Normal file
148
addon/bootstrap.js
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Most of this code is from Zotero team's official Make It Red example[1]
|
||||||
|
* or the Zotero 7 documentation[2].
|
||||||
|
* [1] https://github.com/zotero/make-it-red
|
||||||
|
* [2] https://www.zotero.org/support/dev/zotero_7_for_developers
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (typeof Zotero == "undefined") {
|
||||||
|
var Zotero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromeHandle;
|
||||||
|
|
||||||
|
// In Zotero 6, bootstrap methods are called before Zotero is initialized, and using include.js
|
||||||
|
// to get the Zotero XPCOM service would risk breaking Zotero startup. Instead, wait for the main
|
||||||
|
// Zotero window to open and get the Zotero object from there.
|
||||||
|
//
|
||||||
|
// In Zotero 7, bootstrap methods are not called until Zotero is initialized, and the 'Zotero' is
|
||||||
|
// automatically made available.
|
||||||
|
async function waitForZotero() {
|
||||||
|
if (typeof Zotero != "undefined") {
|
||||||
|
await Zotero.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
|
var windows = Services.wm.getEnumerator("navigator:browser");
|
||||||
|
var found = false;
|
||||||
|
while (windows.hasMoreElements()) {
|
||||||
|
let win = windows.getNext();
|
||||||
|
if (win.Zotero) {
|
||||||
|
Zotero = win.Zotero;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
var listener = {
|
||||||
|
onOpenWindow: function (aWindow) {
|
||||||
|
// Wait for the window to finish loading
|
||||||
|
let domWindow = aWindow
|
||||||
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
|
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
||||||
|
domWindow.addEventListener(
|
||||||
|
"load",
|
||||||
|
function () {
|
||||||
|
domWindow.removeEventListener("load", arguments.callee, false);
|
||||||
|
if (domWindow.Zotero) {
|
||||||
|
Services.wm.removeListener(listener);
|
||||||
|
Zotero = domWindow.Zotero;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Services.wm.addListener(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Zotero.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(data, reason) {}
|
||||||
|
|
||||||
|
async function startup({ id, version, resourceURI, rootURI }, reason) {
|
||||||
|
await waitForZotero();
|
||||||
|
|
||||||
|
// String 'rootURI' introduced in Zotero 7
|
||||||
|
if (!rootURI) {
|
||||||
|
rootURI = resourceURI.spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Zotero.platformMajorVersion >= 102) {
|
||||||
|
var aomStartup = Components.classes[
|
||||||
|
"@mozilla.org/addons/addon-manager-startup;1"
|
||||||
|
].getService(Components.interfaces.amIAddonManagerStartup);
|
||||||
|
var manifestURI = Services.io.newURI(rootURI + "manifest.json");
|
||||||
|
chromeHandle = aomStartup.registerChrome(manifestURI, [
|
||||||
|
["content", "__addonRef__", rootURI + "chrome/content/"],
|
||||||
|
["locale", "__addonRef__", "en-US", rootURI + "chrome/locale/en-US/"],
|
||||||
|
["locale", "__addonRef__", "zh-CN", rootURI + "chrome/locale/zh-CN/"],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setDefaultPrefs(rootURI);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global variables for plugin code
|
||||||
|
const ctx = {
|
||||||
|
rootURI,
|
||||||
|
};
|
||||||
|
ctx._globalThis = ctx;
|
||||||
|
|
||||||
|
Services.scriptloader.loadSubScript(
|
||||||
|
`${rootURI}/chrome/content/scripts/index.js`,
|
||||||
|
ctx,
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown({ id, version, resourceURI, rootURI }, reason) {
|
||||||
|
if (reason === APP_SHUTDOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof Zotero === "undefined") {
|
||||||
|
Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
|
||||||
|
Components.interfaces.nsISupports
|
||||||
|
).wrappedJSObject;
|
||||||
|
}
|
||||||
|
Zotero.__addonInstance__.hooks.onShutdown();
|
||||||
|
|
||||||
|
Cc["@mozilla.org/intl/stringbundle;1"]
|
||||||
|
.getService(Components.interfaces.nsIStringBundleService)
|
||||||
|
.flushBundles();
|
||||||
|
|
||||||
|
Cu.unload(`${rootURI}/chrome/content/scripts/index.js`);
|
||||||
|
|
||||||
|
if (chromeHandle) {
|
||||||
|
chromeHandle.destruct();
|
||||||
|
chromeHandle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninstall(data, reason) {}
|
||||||
|
|
||||||
|
// Loads default preferences from defaults/preferences/prefs.js in Zotero 6
|
||||||
|
function setDefaultPrefs(rootURI) {
|
||||||
|
var branch = Services.prefs.getDefaultBranch("");
|
||||||
|
var obj = {
|
||||||
|
pref(pref, value) {
|
||||||
|
switch (typeof value) {
|
||||||
|
case "boolean":
|
||||||
|
branch.setBoolPref(pref, value);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
branch.setStringPref(pref, value);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
branch.setIntPref(pref, value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Zotero.logError(`Invalid type '${typeof value}' for pref '${pref}'`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Zotero.getMainWindow().console.log(rootURI + "prefs.js");
|
||||||
|
Services.scriptloader.loadSubScript(rootURI + "prefs.js", obj);
|
||||||
|
}
|
3
addon/chrome.manifest
Normal file
3
addon/chrome.manifest
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
content __addonRef__ chrome/content/
|
||||||
|
locale __addonRef__ en-US chrome/locale/en-US/
|
||||||
|
locale __addonRef__ zh-CN chrome/locale/zh-CN/
|
BIN
addon/chrome/content/icons/favicon.png
Normal file
BIN
addon/chrome/content/icons/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 677 B |
BIN
addon/chrome/content/icons/favicon@0.5x.png
Normal file
BIN
addon/chrome/content/icons/favicon@0.5x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 836 B |
259
addon/chrome/content/preferences.xhtml
Normal file
259
addon/chrome/content/preferences.xhtml
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<vbox
|
||||||
|
id="zotero-prefpane-__addonRef__"
|
||||||
|
onload="Zotero.__addonInstance__.hooks.onPrefsLoad(event)"
|
||||||
|
>
|
||||||
|
<groupbox>
|
||||||
|
<label><html:h2>&zotero.__addonRef__.pref.general.label;</html:h2></label>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enableAuto"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enableAuto.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableAuto"
|
||||||
|
/>
|
||||||
|
<hbox>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enableComment"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enableComment.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableComment"
|
||||||
|
/>
|
||||||
|
<radiogroup
|
||||||
|
id="__addonRef__-annotationTranslationPosition"
|
||||||
|
class="auto-annotation"
|
||||||
|
orient="horizontal"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.annotationTranslationPosition"
|
||||||
|
>
|
||||||
|
<radio
|
||||||
|
label="&zotero.__addonRef__.pref.basic.annotationTranslationInComment.label;"
|
||||||
|
value="comment"
|
||||||
|
/>
|
||||||
|
<radio
|
||||||
|
label="&zotero.__addonRef__.pref.basic.annotationTranslationInBody.label;"
|
||||||
|
value="body"
|
||||||
|
/>
|
||||||
|
</radiogroup>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enablePopup"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enablePopup.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enablePopup"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enableHidePopupTextarea"
|
||||||
|
class="enable-popup"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enableHidePopupTextarea.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableHidePopupTextarea"
|
||||||
|
/>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enableAddToNote"
|
||||||
|
class="enable-popup"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enableNote.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableNote"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-enableNoteReplaceMode"
|
||||||
|
class="enable-popup enable-popup-addtonote"
|
||||||
|
label="&zotero.__addonRef__.pref.basic.enableNoteReplaceMode.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableNoteReplaceMode"
|
||||||
|
/>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.audio.autoPlay;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.autoPlay"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.audio.showPlayBtn;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showPlayBtn"
|
||||||
|
/>
|
||||||
|
</hbox>
|
||||||
|
</groupbox>
|
||||||
|
<groupbox>
|
||||||
|
<label><html:h2>&zotero.__addonRef__.pref.service.label;</html:h2></label>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
id="tttest"
|
||||||
|
value="&zotero.__addonRef__.pref.service.sentenceServices.label;"
|
||||||
|
></label>
|
||||||
|
<!-- Replace -->
|
||||||
|
<box id="__addonRef__-sentenceServices-placeholder"></box>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.service.sentenceServicesSecret.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
type="text"
|
||||||
|
id="__addonRef__-sentenceServicesSecret"
|
||||||
|
></html:input>
|
||||||
|
<button id="__addonRef__-sentenceServicesStatus"></button>
|
||||||
|
</hbox>
|
||||||
|
<checkbox
|
||||||
|
id="__addonRef__-useWordService"
|
||||||
|
label="&zotero.__addonRef__.pref.service.useWordService.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.enableDict"
|
||||||
|
/>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.service.wordServices.label;"
|
||||||
|
></label>
|
||||||
|
<!-- Replace -->
|
||||||
|
<box id="__addonRef__-wordServices-placeholder"></box>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.service.wordServicesSecret.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
type="text"
|
||||||
|
id="__addonRef__-wordServicesSecret"
|
||||||
|
class="use-word-service"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<label value="&zotero.__addonRef__.pref.service.langfrom.label;"></label>
|
||||||
|
<!-- Replace -->
|
||||||
|
<box id="__addonRef__-langfrom-placeholder"></box>
|
||||||
|
<label value="&zotero.__addonRef__.pref.service.langto.label;"></label>
|
||||||
|
<!-- Replace -->
|
||||||
|
<box id="__addonRef__-langto-placeholder"></box>
|
||||||
|
</hbox>
|
||||||
|
<label value="&zotero.__addonRef__.pref.service.hint.label;"></label>
|
||||||
|
</groupbox>
|
||||||
|
</vbox>
|
||||||
|
<groupbox>
|
||||||
|
<label><html:h2>&zotero.__addonRef__.pref.interface.label;</html:h2></label>
|
||||||
|
<hbox>
|
||||||
|
<label value="&zotero.__addonRef__.pref.interface.fontSize.label;"></label>
|
||||||
|
<html:input
|
||||||
|
id="__addonRef__-fontSize"
|
||||||
|
type="text"
|
||||||
|
preference="__prefsPrefix__.fontSize"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.interface.lineHeight.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
id="__addonRef__-lineHeight"
|
||||||
|
type="text"
|
||||||
|
preference="__prefsPrefix__.lineHeight"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.autoFocus.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.autoFocus"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarEngine.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarEngine"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarSettings.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarSettings"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarConcat.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarConcat"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarLanguage.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarLanguage"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarRaw.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarRaw"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.rawResultOrder.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.rawResultOrder"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showSidebarCopy.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showSidebarCopy"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showItemBoxTitleTranslation.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showItemBoxTitleTranslation"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.showItemBoxAbstractTranslation.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.showItemBoxAbstractTranslation"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.keepWindowTop.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.keepWindowTop"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
label="&zotero.__addonRef__.pref.interface.keepPopupSize.label;"
|
||||||
|
native="true"
|
||||||
|
preference="__prefsPrefix__.keepPopupSize"
|
||||||
|
/>
|
||||||
|
</groupbox>
|
||||||
|
<groupbox>
|
||||||
|
<label><html:h2>&zotero.__addonRef__.pref.advanced.label;</html:h2></label>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.advanced.disabledLanguages.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
type="text"
|
||||||
|
preference="__prefsPrefix__.disabledLanguages"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.advanced.extraEngines.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
type="text"
|
||||||
|
preference="__prefsPrefix__.extraEngines"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.pref.advanced.splitChar.label;"
|
||||||
|
></label>
|
||||||
|
<html:input
|
||||||
|
type="text"
|
||||||
|
preference="__prefsPrefix__.splitChar"
|
||||||
|
></html:input>
|
||||||
|
</hbox>
|
||||||
|
</groupbox>
|
||||||
|
<groupbox>
|
||||||
|
<label><html:h2>&zotero.__addonRef__.pref.about.label;</html:h2></label>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.help.version.label; &zotero.__addonRef__.help.releasetime.label;"
|
||||||
|
></label>
|
||||||
|
<hbox>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.help.feedback.label;"
|
||||||
|
class="zotero-text-link"
|
||||||
|
href="https://github.com/windingwind/zotero-pdf-translate/issues"
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
value="&zotero.__addonRef__.help.docs.label;"
|
||||||
|
class="zotero-text-link"
|
||||||
|
href="https://zotero.yuque.com/staff-gkhviy/pdf-trans"
|
||||||
|
></label>
|
||||||
|
</hbox>
|
||||||
|
</groupbox>
|
28
addon/chrome/content/standalone.xhtml
Normal file
28
addon/chrome/content/standalone.xhtml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE window>
|
||||||
|
|
||||||
|
<?xml-stylesheet href="chrome://global/skin/global.css"?>
|
||||||
|
<?xml-stylesheet href="chrome://zotero/skin/zotero.css"?>
|
||||||
|
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
|
||||||
|
<?xml-stylesheet href="chrome://zotero-platform-version/content/style.css"?>
|
||||||
|
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||||
|
|
||||||
|
<window
|
||||||
|
id="__addonRef__-standalone"
|
||||||
|
title="Translate Panel"
|
||||||
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||||
|
windowtype="__addonRef__-standalone"
|
||||||
|
persist="screenX screenY width height"
|
||||||
|
onload="window.arguments[0].loadLock.resolve()"
|
||||||
|
>
|
||||||
|
<html:style>
|
||||||
|
#zoteropdftranslate-standalone-panel-openwindow { display: none; }
|
||||||
|
</html:style>
|
||||||
|
<hbox id="panel-container" flex="1"></hbox>
|
||||||
|
<vbox
|
||||||
|
id="extra-container"
|
||||||
|
flex="1"
|
||||||
|
style="padding: 0px 10px 10px 10px"
|
||||||
|
></vbox>
|
||||||
|
</window>
|
95
addon/chrome/locale/en-US/addon.properties
Normal file
95
addon/chrome/locale/en-US/addon.properties
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
readerpanel.label=Translate
|
||||||
|
readerpanel.translate.button.label=Translate
|
||||||
|
readerpanel.auto.description.label=Auto-Trans:
|
||||||
|
readerpanel.auto.selection.label=Selection
|
||||||
|
readerpanel.auto.annotation.label=Annotation
|
||||||
|
readerpanel.concat.description.label=Selection:
|
||||||
|
readerpanel.concat.enable.label=Concat Mode
|
||||||
|
readerpanel.concat.clear.label=Clear
|
||||||
|
readerpanel.copy.description.label=Copy:
|
||||||
|
readerpanel.copy.raw.label=Raw
|
||||||
|
readerpanel.copy.result.label=Result
|
||||||
|
readerpanel.copy.both.label=Both
|
||||||
|
readerpanel.openwindow.open.label=Open Standalone Panel
|
||||||
|
readerpanel.extra.addservice.label=Add Result Source
|
||||||
|
readerpanel.extra.removeservice.label=Remove
|
||||||
|
readerpanel.extra.resize.label=Reset Width
|
||||||
|
readerpanel.extra.pin.label=📌Pin
|
||||||
|
readerpanel.extra.pinned.label=📍Unpin
|
||||||
|
|
||||||
|
service.googleapi=Google(API)
|
||||||
|
service.google=Google
|
||||||
|
service.cnki=CNKI
|
||||||
|
service.youdao=Youdao
|
||||||
|
service.youdaozhiyun=Youdao Zhiyun🗝️
|
||||||
|
service.niutranspro=Niu Trans🗝️
|
||||||
|
service.microsoft=Microsoft🗝️
|
||||||
|
service.caiyun=Caiyun🗝️
|
||||||
|
service.deeplfree=DeepL(Free Plan)🗝️
|
||||||
|
service.deeplpro=DeepL(Pro Plan)🗝️
|
||||||
|
service.deeplcustom=DeepL(Custom)🗝️
|
||||||
|
service.deeplx=DeepLx
|
||||||
|
service.baidu=Baidu🗝️
|
||||||
|
service.baidufield=Baidu Field🗝️
|
||||||
|
service.openl=OpenL🗝️
|
||||||
|
service.tencent=Tencent🗝️
|
||||||
|
service.xftrans=Xftrans🗝️
|
||||||
|
service.gpt=GPT🗝️
|
||||||
|
service.haici=Haici
|
||||||
|
service.bingdict=Bing Dict(en↔zh)🔊
|
||||||
|
service.haicidict=Haici Dict(en↔zh)🔊
|
||||||
|
service.collinsdict=Collins Dict(en↔zh)🔊
|
||||||
|
service.youdaodict=Haici Dict(en↔zh)
|
||||||
|
service.freedictionaryapi=FreeDictionaryAPI(en↔en)
|
||||||
|
service.webliodict=weblio(en↔ja)
|
||||||
|
service.errorPrefix=[Request Error]\n\nEngine not available, invalid secret, or request too fast.\nUse another translation engine or post the issue here: \n https://github.com/windingwind/zotero-pdf-translate/issues \n\nThe message below is not Zotero or the PDF Translate addon, but from
|
||||||
|
|
||||||
|
service.niutranspro.secret.pass=More...
|
||||||
|
service.niutranspro.secret.fail=Login
|
||||||
|
service.niutranspro.dialog.title=NiuTrans Account
|
||||||
|
service.niutranspro.dialog.username=Username
|
||||||
|
service.niutranspro.dialog.password=Password
|
||||||
|
service.niutranspro.dialog.signup=Sign up
|
||||||
|
service.niutranspro.dialog.forget=Forget
|
||||||
|
service.niutranspro.dialog.dictLib=Dict Lib
|
||||||
|
service.niutranspro.dialog.memoryLib=Memory Lib
|
||||||
|
service.niutranspro.dialog.tip0=Please go to the
|
||||||
|
service.niutranspro.dialog.tip1=Niutrans Cloud Platform
|
||||||
|
service.niutranspro.dialog.tip2=to add the term dictionary library
|
||||||
|
service.niutranspro.dialog.signin=Sign In
|
||||||
|
service.niutranspro.dialog.refresh=Refresh
|
||||||
|
service.niutranspro.dialog.signout=Sign Out
|
||||||
|
service.niutranspro.dialog.close=Close
|
||||||
|
|
||||||
|
service.deeplcustom.secret.pass=Docs
|
||||||
|
service.deeplcustom.secret.fail=Docs
|
||||||
|
|
||||||
|
service.gpt.secret.pass=Config
|
||||||
|
service.gpt.secret.fail=Config
|
||||||
|
service.gpt.dialog.title=GPT Config
|
||||||
|
service.gpt.dialog.url=API
|
||||||
|
service.gpt.dialog.models=Model
|
||||||
|
service.gpt.dialog.temperature=Temp
|
||||||
|
service.gpt.dialog.status=Status
|
||||||
|
service.gpt.dialog.help=help
|
||||||
|
service.gpt.dialog.save=save
|
||||||
|
service.gpt.dialog.close=close
|
||||||
|
service.gpt.dialog.status.load=loading
|
||||||
|
service.gpt.dialog.status.available=available
|
||||||
|
service.gpt.dialog.status.timeout=timeout
|
||||||
|
service.gpt.dialog.status.invalid=secret invalid
|
||||||
|
service.gpt.dialog.status.unexpect=fail
|
||||||
|
|
||||||
|
readerpopup.translate.label=Translate
|
||||||
|
|
||||||
|
pref.title=Translate
|
||||||
|
|
||||||
|
itemmenu.translateTitle.label=Translate Title
|
||||||
|
itemmenu.switchTitleMode.label=Switch Original Title/Translation
|
||||||
|
itemmenu.translateAbstract.label=Translate Abstract
|
||||||
|
itemmenu.more.label=More Translation Options...
|
||||||
|
itemmenu.retranslateTitle.label=Retranslate Title
|
||||||
|
itemmenu.retranslateAbstract.label=Retranslate Abstract
|
||||||
|
|
||||||
|
field.titleTranslation=Title Translation
|
||||||
|
field.abstractTranslation=Abstract Translation
|
51
addon/chrome/locale/en-US/overlay.dtd
Normal file
51
addon/chrome/locale/en-US/overlay.dtd
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!ENTITY zotero.__addonRef__.pref.general.label "General">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableAuto.label "Auto-Trans Selection">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enablePopup.label "Enable Popup">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableHidePopupTextarea.label "Hide Popup Textarea">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableComment.label "Auto-Trans Annotation">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.annotationTranslationInComment.label "Save to Annotation Comment">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.annotationTranslationInBody.label "Save to Annotation Body">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableNote.label "Show 'Add Translation to Note' in Popup">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableNoteReplaceMode.label "Replace Raw when Adding to Note">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.audio.autoPlay "Auto play prononciation">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.audio.showPlayBtn "Show play buttons in popup panel">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.label "Service">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.sentenceServices.label "Translation Services">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.sentenceServicesSecret.label "Secret">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.useWordService.label "Use Dictionary Service for Word Translation">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.wordServices.label "Dictionary Services">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.wordServicesSecret.label "Secret">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.langfrom.label "Translate From">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.langto.label "To">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.hint.label "Translate Engine with 🗝️ requires Secret; See GitHub for more information">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.help.feedback.caption.label "User Guide and Feedback">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.feedback.label "GitHub">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.docs.label "Documentation(ZH)">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.version.label "Zotero PDF Translate VERSION __buildVersion__">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.label "User Interface">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.fontSize.label "Font Size">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.lineHeight.label "Line Height">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.autoFocus.label "SideBar: Auto Focus">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarEngine.label "SideBar: Show Engine Selection Menu">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarSettings.label "SideBar: Show Settings">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarConcat.label "SideBar: Show Concat Selection Menu">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarLanguage.label "SideBar: Show Language Selection Menu">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarRaw.label "SideBar: Show Raw">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarCopy.label "SideBar: Show Copy Buttons">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.rawResultOrder.label "SideBar: Reverse Raw/Result">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showItemBoxTitleTranslation.label "Info Panel: Show Title Translation">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showItemBoxAbstractTranslation.label "Info Panel: Show Abstract Translation">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.keepWindowTop.label "Standalone: Keep Window on Top">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.keepPopupSize.label "Popup: Remember Size">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.label "Advanced">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.disabledLanguages.label "Disable Automatic Translation when File Language is(split with ',')">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.disabledLanguages.alert "Reopen files/restart Zotero to apply.">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.extraEngines.label "Extra engines in standalone window(split with ',')">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.splitChar.label "Split Character(between text and translation)">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.about.label "About">
|
95
addon/chrome/locale/zh-CN/addon.properties
Normal file
95
addon/chrome/locale/zh-CN/addon.properties
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
readerpanel.label=翻译
|
||||||
|
readerpanel.translate.button.label=翻译
|
||||||
|
readerpanel.auto.description.label=自动翻译:
|
||||||
|
readerpanel.auto.selection.label=选择内容
|
||||||
|
readerpanel.auto.annotation.label=批注
|
||||||
|
readerpanel.concat.description.label=选择内容:
|
||||||
|
readerpanel.concat.enable.label=拼接模式
|
||||||
|
readerpanel.concat.clear.label=清空
|
||||||
|
readerpanel.copy.description.label=复制:
|
||||||
|
readerpanel.copy.raw.label=源文本
|
||||||
|
readerpanel.copy.result.label=结果
|
||||||
|
readerpanel.copy.both.label=两者
|
||||||
|
readerpanel.openwindow.open.label=打开独立窗口
|
||||||
|
readerpanel.extra.addservice.label=添加结果来源
|
||||||
|
readerpanel.extra.removeservice.label=移除
|
||||||
|
readerpanel.extra.resize.label=重置宽度
|
||||||
|
readerpanel.extra.pin.label=📌置顶
|
||||||
|
readerpanel.extra.pinned.label=📍取消置顶
|
||||||
|
|
||||||
|
service.googleapi=Google(API)
|
||||||
|
service.google=Google
|
||||||
|
service.cnki=CNKI
|
||||||
|
service.youdao=有道
|
||||||
|
service.youdaozhiyun=有道智云🗝️
|
||||||
|
service.niutranspro=小牛🗝️
|
||||||
|
service.microsoft=微软🗝️
|
||||||
|
service.caiyun=彩云🗝️
|
||||||
|
service.deeplfree=DeepL(免费订阅)🗝️
|
||||||
|
service.deeplpro=DeepL(Pro订阅)🗝️
|
||||||
|
service.deeplcustom=DeepL(自定义)🗝️
|
||||||
|
service.deeplx=DeepLx
|
||||||
|
service.baidu=百度🗝️
|
||||||
|
service.baidufield=百度垂直领域🗝️
|
||||||
|
service.openl=OpenL🗝️
|
||||||
|
service.tencent=腾讯🗝️
|
||||||
|
service.xftrans=讯飞🗝️
|
||||||
|
service.gpt=GPT🗝️
|
||||||
|
service.haici=海词
|
||||||
|
service.bingdict=必应词典(en↔zh)🔊
|
||||||
|
service.haicidict=海词词典(en↔zh)🔊
|
||||||
|
service.collinsdict=科林斯词典(en↔zh)🔊
|
||||||
|
service.youdaodict=有道词典(en↔zh)
|
||||||
|
service.freedictionaryapi=FreeDictionaryAPI(en↔en)
|
||||||
|
service.webliodict=Weblio Dict(en↔ja)
|
||||||
|
service.errorPrefix=[请求错误]\n\n此翻译引擎不可用,可能是密钥错误,也可能是请求过快。\n可以尝试其他翻译引擎,或者来此查看相关回答:\nhttps://zotero.yuque.com/staff-gkhviy/pdf-trans/age09f \n\n请注意,这些错误与 Zotero 和本翻译插件无关,由该翻译服务引起:
|
||||||
|
|
||||||
|
service.niutranspro.secret.pass=更多...
|
||||||
|
service.niutranspro.secret.fail=登录
|
||||||
|
service.niutranspro.dialog.title=小牛翻译账户
|
||||||
|
service.niutranspro.dialog.username=用户名
|
||||||
|
service.niutranspro.dialog.password=密码
|
||||||
|
service.niutranspro.dialog.signup=注册
|
||||||
|
service.niutranspro.dialog.forget=忘记密码
|
||||||
|
service.niutranspro.dialog.dictLib=术语词典
|
||||||
|
service.niutranspro.dialog.memoryLib=翻译记忆
|
||||||
|
service.niutranspro.dialog.tip0=请到
|
||||||
|
service.niutranspro.dialog.tip1=小牛翻译云平台
|
||||||
|
service.niutranspro.dialog.tip2=进行添加术语词典库
|
||||||
|
service.niutranspro.dialog.signin=登录
|
||||||
|
service.niutranspro.dialog.refresh=刷新
|
||||||
|
service.niutranspro.dialog.signout=退出登录
|
||||||
|
service.niutranspro.dialog.close=关闭
|
||||||
|
|
||||||
|
service.deeplcustom.secret.pass=文档
|
||||||
|
service.deeplcustom.secret.fail=文档
|
||||||
|
|
||||||
|
service.gpt.secret.pass=配置
|
||||||
|
service.gpt.secret.fail=配置
|
||||||
|
service.gpt.dialog.title=GPT 配置项
|
||||||
|
service.gpt.dialog.url=接口
|
||||||
|
service.gpt.dialog.models=模型
|
||||||
|
service.gpt.dialog.temperature=温度
|
||||||
|
service.gpt.dialog.status=状态
|
||||||
|
service.gpt.dialog.help=帮助
|
||||||
|
service.gpt.dialog.save=保存
|
||||||
|
service.gpt.dialog.close=关闭
|
||||||
|
service.gpt.dialog.status.load=加载中
|
||||||
|
service.gpt.dialog.status.available=可用
|
||||||
|
service.gpt.dialog.status.timeout=请求超时
|
||||||
|
service.gpt.dialog.status.invalid=密钥不可用
|
||||||
|
service.gpt.dialog.status.unexpect=服务不可用
|
||||||
|
|
||||||
|
readerpopup.translate.label=翻译
|
||||||
|
|
||||||
|
pref.title=翻译
|
||||||
|
|
||||||
|
itemmenu.translateTitle.label=翻译标题
|
||||||
|
itemmenu.switchTitleMode.label=切换原标题/翻译
|
||||||
|
itemmenu.translateAbstract.label=翻译摘要
|
||||||
|
itemmenu.more.label=更多翻译选项...
|
||||||
|
itemmenu.retranslateTitle.label=重新翻译标题
|
||||||
|
itemmenu.retranslateAbstract.label=重新翻译摘要
|
||||||
|
|
||||||
|
field.titleTranslation=标题翻译
|
||||||
|
field.abstractTranslation=摘要翻译
|
51
addon/chrome/locale/zh-CN/overlay.dtd
Normal file
51
addon/chrome/locale/zh-CN/overlay.dtd
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!ENTITY zotero.__addonRef__.pref.general.label "通用">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableAuto.label "自动翻译选择内容">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enablePopup.label "启用弹窗">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableHidePopupTextarea.label "隐藏翻译弹窗">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableComment.label "自动翻译批注">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.annotationTranslationInComment.label "存在笔记评论">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.annotationTranslationInBody.label "存在笔记本身">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableNote.label "在弹窗显示'添加翻译到笔记'">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.basic.enableNoteReplaceMode.label "添加到笔记时替换原始文本">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.audio.autoPlay "自动播放发音">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.audio.showPlayBtn "弹窗中显示播放键">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.label "服务">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.sentenceServices.label "翻译服务">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.sentenceServicesSecret.label "密钥">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.useWordService.label "使用字典服务翻译词语">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.wordServices.label "字典服务">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.wordServicesSecret.label "密钥">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.langfrom.label "从">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.langto.label "翻译到">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.service.hint.label "带🗝️翻译服务需要密钥; 请参阅GitHub">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.help.feedback.caption.label "使用方法及问题反馈">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.feedback.label "GitHub">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.docs.label "文档(中文)">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.version.label "Zotero PDF Translate 版本 __buildVersion__">
|
||||||
|
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.label "用户界面">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.fontSize.label "字体大小">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.lineHeight.label "行高">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.autoFocus.label "侧边栏: 自动聚焦">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarEngine.label "侧边栏: 显示翻译引擎选择菜单">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarLanguage.label "侧边栏: 显示语言选择菜单">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarSettings.label "侧边栏: 显示设置">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarConcat.label "侧边栏: 显示拼接文本菜单">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarRaw.label "侧边栏: 显示源文本">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showSidebarCopy.label "侧边栏: 显示复制按钮">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.rawResultOrder.label "侧边栏: 反转源/结果显示">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showItemBoxTitleTranslation.label "信息边栏: 显示标题翻译">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.showItemBoxAbstractTranslation.label "信息边栏:显示摘要翻译">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.keepWindowTop.label "独立窗口: 保持最前">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.interface.keepPopupSize.label "弹窗: 记住大小">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.label "高级">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.disabledLanguages.label "对特定语言文件关闭自动翻译(用','分隔)">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.disabledLanguages.alert "重新打开文件/重启Zotero以应用更改。">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.extraEngines.label "独立窗口额外翻译引擎(用','分隔)">
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.advanced.splitChar.label "分隔符(原文与翻译之间)">
|
||||||
|
|
||||||
|
<!ENTITY zotero.__addonRef__.pref.about.label "关于">
|
35
addon/install.rdf
Normal file
35
addon/install.rdf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<RDF:RDF
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#"
|
||||||
|
xmlns:NC="http://home.netscape.com/NC-rdf#"
|
||||||
|
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<RDF:Description
|
||||||
|
RDF:about="urn:mozilla:install-manifest"
|
||||||
|
em:id="__addonID__"
|
||||||
|
em:name="__addonName__"
|
||||||
|
em:version="__buildVersion__"
|
||||||
|
em:type="2"
|
||||||
|
em:creator="__author__"
|
||||||
|
em:description="__description__"
|
||||||
|
em:homepageURL="__homepage__"
|
||||||
|
em:iconURL="chrome://__addonRef__/content/icons/favicon.png"
|
||||||
|
em:optionsURL="chrome://__addonRef__/content/preferences.xul"
|
||||||
|
em:updateURL="__updaterdf__"
|
||||||
|
em:multiprocessCompatible="true"
|
||||||
|
em:bootstrap="true">
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||||
|
<em:minVersion>5.0</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<em:id>juris-m@juris-m.github.io</em:id>
|
||||||
|
<em:minVersion>5.0</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
</RDF:Description>
|
||||||
|
</RDF:RDF>
|
19
addon/manifest.json
Normal file
19
addon/manifest.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "__addonName__",
|
||||||
|
"version": "__buildVersion__",
|
||||||
|
"description": "__description__",
|
||||||
|
"author": "__author__",
|
||||||
|
"icons": {
|
||||||
|
"48": "chrome/content/icons/favicon@0.5x.png",
|
||||||
|
"96": "chrome/content/icons/favicon.png"
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"zotero": {
|
||||||
|
"id": "__addonID__",
|
||||||
|
"update_url": "__updaterdf__",
|
||||||
|
"strict_min_version": "6.0.*",
|
||||||
|
"strict_max_version": "6.999"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
addon/prefs.js
Normal file
42
addon/prefs.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
pref("__prefsPrefix__.enableAuto", true);
|
||||||
|
pref("__prefsPrefix__.enableDict", true);
|
||||||
|
pref("__prefsPrefix__.enablePopup", true);
|
||||||
|
pref("__prefsPrefix__.enableComment", true);
|
||||||
|
pref("__prefsPrefix__.annotationTranslationPosition", "comment");
|
||||||
|
pref("__prefsPrefix__.enableNote", true);
|
||||||
|
pref("__prefsPrefix__.enableNoteReplaceMode", false);
|
||||||
|
pref("__prefsPrefix__.translateSource", "");
|
||||||
|
pref("__prefsPrefix__.dictSource", "");
|
||||||
|
pref("__prefsPrefix__.sourceLanguage", "en-US");
|
||||||
|
pref("__prefsPrefix__.targetLanguage", "");
|
||||||
|
pref("__prefsPrefix__.fontSize", "12");
|
||||||
|
pref("__prefsPrefix__.lineHeight", "1.5");
|
||||||
|
pref("__prefsPrefix__.splitChar", "\ud83d\udd24");
|
||||||
|
pref("__prefsPrefix__.autoFocus", true);
|
||||||
|
pref("__prefsPrefix__.rawResultOrder", false);
|
||||||
|
pref("__prefsPrefix__.showSidebarEngine", true);
|
||||||
|
pref("__prefsPrefix__.showSidebarSettings", true);
|
||||||
|
pref("__prefsPrefix__.showSidebarConcat", true);
|
||||||
|
pref("__prefsPrefix__.showSidebarLanguage", true);
|
||||||
|
pref("__prefsPrefix__.showSidebarRaw", true);
|
||||||
|
pref("__prefsPrefix__.showSidebarCopy", true);
|
||||||
|
pref("__prefsPrefix__.showItemBoxTitleTranslation", true);
|
||||||
|
pref("__prefsPrefix__.showItemBoxAbstractTranslation", true);
|
||||||
|
pref("__prefsPrefix__.keepWindowTop", false);
|
||||||
|
pref("__prefsPrefix__.keepPopupSize", false);
|
||||||
|
pref("__prefsPrefix__.popupWidth", 105);
|
||||||
|
pref("__prefsPrefix__.popupHeight", 30);
|
||||||
|
pref("__prefsPrefix__.niutransUsername", "");
|
||||||
|
pref("__prefsPrefix__.niutransPassword", "");
|
||||||
|
pref("__prefsPrefix__.niutransDictNo", "");
|
||||||
|
pref("__prefsPrefix__.niutransMemoryNo", "");
|
||||||
|
pref("__prefsPrefix__.niutransDictLibList", "[]");
|
||||||
|
pref("__prefsPrefix__.niutransMemoryLibList", "[]");
|
||||||
|
pref("__prefsPrefix__.autoPlay", false);
|
||||||
|
pref("__prefsPrefix__.showPlayBtn", true);
|
||||||
|
pref("__prefsPrefix__.disabledLanguages", "");
|
||||||
|
pref("__prefsPrefix__.extraEngines", "");
|
||||||
|
pref("__prefsPrefix__.titleColumnMode", "raw");
|
||||||
|
pref("__prefsPrefix__.gptUrl", "https://api.openai.com/v1/chat/completions");
|
||||||
|
pref("__prefsPrefix__.gptModel", "gpt-3.5-turbo");
|
||||||
|
pref("__prefsPrefix__.gptTemperature", "1.0");
|
57
package.json
Normal file
57
package.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "zotero-pdf-translate",
|
||||||
|
"version": "1.0.25",
|
||||||
|
"description": "PDF translation for Zotero built-in PDF reader.",
|
||||||
|
"config": {
|
||||||
|
"addonName": "Zotero PDF Translate",
|
||||||
|
"addonID": "zoteropdftranslate@euclpts.com",
|
||||||
|
"addonRef": "zoteropdftranslate",
|
||||||
|
"prefsPrefix": "extensions.zotero.ZoteroPDFTranslate",
|
||||||
|
"addonInstance": "PDFTranslate",
|
||||||
|
"releasepage": "https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi",
|
||||||
|
"updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-pdf-translate/main/update.json"
|
||||||
|
},
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build-dev": "cross-env NODE_ENV=development node scripts/build.js",
|
||||||
|
"build-prod": "cross-env NODE_ENV=production node scripts/build.js",
|
||||||
|
"build": "concurrently -c auto npm:build-prod npm:tsc",
|
||||||
|
"tsc": "tsc --noEmit",
|
||||||
|
"start-z6": "node scripts/start.js --z 6",
|
||||||
|
"start-z7": "node scripts/start.js --z 7",
|
||||||
|
"start": "node scripts/start.js",
|
||||||
|
"stop": "node scripts/stop.js",
|
||||||
|
"restart-dev-z6": "npm run build-dev && npm run stop && npm run start-z6",
|
||||||
|
"restart-dev-z7": "npm run build-dev && npm run stop && npm run start-z7",
|
||||||
|
"restart-prod": "npm run build-prod && npm run stop && npm run start",
|
||||||
|
"restart": "npm run restart-dev",
|
||||||
|
"release": "release-it",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/windingwind/zotero-pdf-translate.git"
|
||||||
|
},
|
||||||
|
"author": "windingwind",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/windingwind/zotero-pdf-translate/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/windingwind/zotero-pdf-translate#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"jsencrypt": "^3.3.2",
|
||||||
|
"zotero-plugin-toolkit": "^2.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.16.1",
|
||||||
|
"compressing": "^1.9.0",
|
||||||
|
"concurrently": "^8.0.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"esbuild": "^0.17.18",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"release-it": "^15.10.1",
|
||||||
|
"replace-in-file": "^6.3.5",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"zotero-types": "^1.0.13"
|
||||||
|
}
|
||||||
|
}
|
179
scripts/build.js
Normal file
179
scripts/build.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
const esbuild = require("esbuild");
|
||||||
|
const compressing = require("compressing");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const process = require("process");
|
||||||
|
const replace = require("replace-in-file");
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
homepage,
|
||||||
|
version,
|
||||||
|
config,
|
||||||
|
} = require("../package.json");
|
||||||
|
|
||||||
|
function copyFileSync(source, target) {
|
||||||
|
var targetFile = target;
|
||||||
|
|
||||||
|
// If target is a directory, a new file with the same name will be created
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
if (fs.lstatSync(target).isDirectory()) {
|
||||||
|
targetFile = path.join(target, path.basename(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(targetFile, fs.readFileSync(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFolderRecursiveSync(source, target) {
|
||||||
|
var files = [];
|
||||||
|
|
||||||
|
// Check if folder needs to be created or integrated
|
||||||
|
var targetFolder = path.join(target, path.basename(source));
|
||||||
|
if (!fs.existsSync(targetFolder)) {
|
||||||
|
fs.mkdirSync(targetFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy
|
||||||
|
if (fs.lstatSync(source).isDirectory()) {
|
||||||
|
files = fs.readdirSync(source);
|
||||||
|
files.forEach(function (file) {
|
||||||
|
var curSource = path.join(source, file);
|
||||||
|
if (fs.lstatSync(curSource).isDirectory()) {
|
||||||
|
copyFolderRecursiveSync(curSource, targetFolder);
|
||||||
|
} else {
|
||||||
|
copyFileSync(curSource, targetFolder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFolder(target) {
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
fs.rmSync(target, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(target, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateFormat(fmt, date) {
|
||||||
|
let ret;
|
||||||
|
const opt = {
|
||||||
|
"Y+": date.getFullYear().toString(),
|
||||||
|
"m+": (date.getMonth() + 1).toString(),
|
||||||
|
"d+": date.getDate().toString(),
|
||||||
|
"H+": date.getHours().toString(),
|
||||||
|
"M+": date.getMinutes().toString(),
|
||||||
|
"S+": date.getSeconds().toString(),
|
||||||
|
};
|
||||||
|
for (let k in opt) {
|
||||||
|
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||||
|
if (ret) {
|
||||||
|
fmt = fmt.replace(
|
||||||
|
ret[1],
|
||||||
|
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const t = new Date();
|
||||||
|
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t);
|
||||||
|
const buildDir = "builds";
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
|
||||||
|
process.env.NODE_ENV,
|
||||||
|
]}`
|
||||||
|
);
|
||||||
|
|
||||||
|
clearFolder(buildDir);
|
||||||
|
|
||||||
|
copyFolderRecursiveSync("addon", buildDir);
|
||||||
|
|
||||||
|
copyFileSync("update-template.json", "update.json");
|
||||||
|
copyFileSync("update-template.rdf", "update.rdf");
|
||||||
|
|
||||||
|
await esbuild
|
||||||
|
.build({
|
||||||
|
entryPoints: ["src/index.ts"],
|
||||||
|
define: {
|
||||||
|
__env__: `"${process.env.NODE_ENV}"`,
|
||||||
|
},
|
||||||
|
bundle: true,
|
||||||
|
target: "firefox60",
|
||||||
|
outfile: path.join(buildDir, "addon/chrome/content/scripts/index.js"),
|
||||||
|
// Don't turn minify on
|
||||||
|
// minify: true,
|
||||||
|
})
|
||||||
|
.catch(() => process.exit(1));
|
||||||
|
|
||||||
|
console.log("[Build] Run esbuild OK");
|
||||||
|
|
||||||
|
const replaceFrom = [
|
||||||
|
/__author__/g,
|
||||||
|
/__description__/g,
|
||||||
|
/__homepage__/g,
|
||||||
|
/__buildVersion__/g,
|
||||||
|
/__buildTime__/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
const replaceTo = [author, description, homepage, version, buildTime];
|
||||||
|
|
||||||
|
replaceFrom.push(
|
||||||
|
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g"))
|
||||||
|
);
|
||||||
|
replaceTo.push(...Object.values(config));
|
||||||
|
|
||||||
|
const optionsAddon = {
|
||||||
|
files: [
|
||||||
|
path.join(buildDir, "**/*.rdf"),
|
||||||
|
path.join(buildDir, "**/*.dtd"),
|
||||||
|
path.join(buildDir, "**/*.xul"),
|
||||||
|
path.join(buildDir, "**/*.xhtml"),
|
||||||
|
path.join(buildDir, "**/*.json"),
|
||||||
|
path.join(buildDir, "addon/prefs.js"),
|
||||||
|
path.join(buildDir, "addon/chrome.manifest"),
|
||||||
|
path.join(buildDir, "addon/manifest.json"),
|
||||||
|
path.join(buildDir, "addon/bootstrap.js"),
|
||||||
|
"update.json",
|
||||||
|
"update.rdf",
|
||||||
|
],
|
||||||
|
from: replaceFrom,
|
||||||
|
to: replaceTo,
|
||||||
|
countMatches: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = replace.sync(optionsAddon);
|
||||||
|
console.log(
|
||||||
|
"[Build] Run replace in ",
|
||||||
|
_.filter((f) => f.hasChanged).map(
|
||||||
|
(f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Build] Replace OK");
|
||||||
|
|
||||||
|
console.log("[Build] Addon prepare OK");
|
||||||
|
|
||||||
|
compressing.zip.compressDir(
|
||||||
|
path.join(buildDir, "addon"),
|
||||||
|
path.join(buildDir, `${name}.xpi`),
|
||||||
|
{
|
||||||
|
ignoreBase: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Build] Addon pack OK");
|
||||||
|
console.log(
|
||||||
|
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
25
scripts/start.js
Normal file
25
scripts/start.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { execSync } = require("child_process");
|
||||||
|
const { exit } = require("process");
|
||||||
|
const { exec } = require("./zotero-cmd.json");
|
||||||
|
|
||||||
|
// Run node start.js -h for help
|
||||||
|
const args = require("minimist")(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (args.help || args.h) {
|
||||||
|
console.log("Start Zotero Args:");
|
||||||
|
console.log(
|
||||||
|
"--zotero(-z): Zotero exec key in zotero-cmd.json. Default the first one."
|
||||||
|
);
|
||||||
|
console.log("--profile(-p): Zotero profile name.");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoteroPath = exec[args.zotero || args.z || Object.keys(exec)[0]];
|
||||||
|
const profile = args.profile || args.p;
|
||||||
|
|
||||||
|
const startZotero = `${zoteroPath} --debugger --purgecaches ${
|
||||||
|
profile ? `-p ${profile}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
execSync(startZotero);
|
||||||
|
exit(0);
|
10
scripts/stop.js
Normal file
10
scripts/stop.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const { execSync } = require("child_process");
|
||||||
|
const { killZoteroWindows, killZoteroUnix } = require("./zotero-cmd.json");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
execSync(killZoteroWindows);
|
||||||
|
} else {
|
||||||
|
execSync(killZoteroUnix);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
9
scripts/zotero-cmd-default.json
Normal file
9
scripts/zotero-cmd-default.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
||||||
|
"killZoteroWindows": "taskkill /f /im zotero.exe",
|
||||||
|
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)",
|
||||||
|
"exec": {
|
||||||
|
"6": "/path/to/zotero6.exe",
|
||||||
|
"7": "/path/to/zotero7.exe"
|
||||||
|
}
|
||||||
|
}
|
141
src/addon.ts
Normal file
141
src/addon.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import api from "./api";
|
||||||
|
import hooks from "./hooks";
|
||||||
|
import { TranslateTask } from "./utils/translate";
|
||||||
|
import { TranslationServices } from "./modules/services";
|
||||||
|
import { config } from "../package.json";
|
||||||
|
|
||||||
|
class Addon {
|
||||||
|
public data: {
|
||||||
|
alive: boolean;
|
||||||
|
// Env type, see build.js
|
||||||
|
env: "development" | "production";
|
||||||
|
ztoolkit: ZToolkit;
|
||||||
|
locale: {
|
||||||
|
stringBundle: any;
|
||||||
|
};
|
||||||
|
prefs: {
|
||||||
|
window: Window | null;
|
||||||
|
};
|
||||||
|
panel: {
|
||||||
|
tabOptionId: string;
|
||||||
|
activePanels: HTMLElement[];
|
||||||
|
windowPanel: Window | null;
|
||||||
|
};
|
||||||
|
popup: {
|
||||||
|
currentPopup: HTMLDivElement | null;
|
||||||
|
};
|
||||||
|
translate: {
|
||||||
|
concatKey: boolean;
|
||||||
|
concatCheckbox: boolean;
|
||||||
|
queue: TranslateTask[];
|
||||||
|
maximumQueueLength: number;
|
||||||
|
batchTaskDelay: number;
|
||||||
|
services: TranslationServices;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// Lifecycle hooks
|
||||||
|
public hooks: typeof hooks;
|
||||||
|
// APIs
|
||||||
|
public api: typeof api;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.data = {
|
||||||
|
alive: true,
|
||||||
|
env: __env__,
|
||||||
|
ztoolkit: new ZToolkit(),
|
||||||
|
locale: { stringBundle: null },
|
||||||
|
prefs: { window: null },
|
||||||
|
panel: { tabOptionId: "", activePanels: [], windowPanel: null },
|
||||||
|
popup: { currentPopup: null },
|
||||||
|
translate: {
|
||||||
|
concatKey: false,
|
||||||
|
concatCheckbox: false,
|
||||||
|
queue: [],
|
||||||
|
maximumQueueLength: 100,
|
||||||
|
batchTaskDelay: 1000,
|
||||||
|
services: new TranslationServices(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.hooks = hooks;
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternatively, import toolkit modules you use to minify the plugin size.
|
||||||
|
*
|
||||||
|
* Steps to replace the default `ztoolkit: ZoteroToolkit` with your `ztoolkit: MyToolkit`:
|
||||||
|
*
|
||||||
|
* 1. Uncomment this file's line 30: `ztoolkit: new MyToolkit(),`
|
||||||
|
* and comment line 31: `ztoolkit: new ZoteroToolkit(),`.
|
||||||
|
* 2. Uncomment this file's line 10: `ztoolkit: MyToolkit;` in this file
|
||||||
|
* and comment line 11: `ztoolkit: ZoteroToolkit;`.
|
||||||
|
* 3. Uncomment `./typing/global.d.ts` line 12: `declare const ztoolkit: import("../src/addon").MyToolkit;`
|
||||||
|
* and comment line 13: `declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;`.
|
||||||
|
*
|
||||||
|
* You can now add the modules under the `MyToolkit` class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BasicTool, unregister } from "zotero-plugin-toolkit/dist/basic";
|
||||||
|
import { ToolkitGlobal } from "zotero-plugin-toolkit/dist/managers/toolkitGlobal";
|
||||||
|
import { UITool } from "zotero-plugin-toolkit/dist/tools/ui";
|
||||||
|
import { ShortcutManager } from "zotero-plugin-toolkit/dist/managers/shortcut";
|
||||||
|
import { MenuManager } from "zotero-plugin-toolkit/dist/managers/menu";
|
||||||
|
import { PreferencePaneManager } from "zotero-plugin-toolkit/dist/managers/preferencePane";
|
||||||
|
import { ReaderTabPanelManager } from "zotero-plugin-toolkit/dist/managers/readerTabPanel";
|
||||||
|
import { ReaderInstanceManager } from "zotero-plugin-toolkit/dist/managers/readerInstance";
|
||||||
|
import { PromptManager } from "zotero-plugin-toolkit/dist/managers/prompt";
|
||||||
|
import { ProgressWindowHelper } from "zotero-plugin-toolkit/dist/helpers/progressWindow";
|
||||||
|
import { ClipboardHelper } from "zotero-plugin-toolkit/dist/helpers/clipboard";
|
||||||
|
import { ReaderTool } from "zotero-plugin-toolkit/dist/tools/reader";
|
||||||
|
import { ExtraFieldTool } from "zotero-plugin-toolkit/dist/tools/extraField";
|
||||||
|
import { ItemTreeManager } from "zotero-plugin-toolkit/dist/managers/itemTree";
|
||||||
|
import { ItemBoxManager } from "zotero-plugin-toolkit/dist/managers/itemBox";
|
||||||
|
import { DialogHelper } from "zotero-plugin-toolkit/dist/helpers/dialog";
|
||||||
|
|
||||||
|
export class ZToolkit extends BasicTool {
|
||||||
|
Global: typeof ToolkitGlobal;
|
||||||
|
UI: UITool;
|
||||||
|
Reader: ReaderTool;
|
||||||
|
ExtraField: ExtraFieldTool;
|
||||||
|
Shortcut: ShortcutManager;
|
||||||
|
Menu: MenuManager;
|
||||||
|
ItemTree: ItemTreeManager;
|
||||||
|
ItemBox: ItemBoxManager;
|
||||||
|
Prompt: PromptManager;
|
||||||
|
PreferencePane: PreferencePaneManager;
|
||||||
|
ReaderTabPanel: ReaderTabPanelManager;
|
||||||
|
ReaderInstance: ReaderInstanceManager;
|
||||||
|
Dialog: typeof DialogHelper;
|
||||||
|
ProgressWindow: typeof ProgressWindowHelper;
|
||||||
|
Clipboard: typeof ClipboardHelper;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.Global = ToolkitGlobal;
|
||||||
|
this.UI = new UITool(this);
|
||||||
|
this.Reader = new ReaderTool(this);
|
||||||
|
this.ExtraField = new ExtraFieldTool(this);
|
||||||
|
this.Shortcut = new ShortcutManager(this);
|
||||||
|
this.Menu = new MenuManager(this);
|
||||||
|
this.ItemTree = new ItemTreeManager(this);
|
||||||
|
this.ItemBox = new ItemBoxManager(this);
|
||||||
|
this.PreferencePane = new PreferencePaneManager(this);
|
||||||
|
this.ReaderTabPanel = new ReaderTabPanelManager(this);
|
||||||
|
this.ReaderInstance = new ReaderInstanceManager(this);
|
||||||
|
this.Prompt = new PromptManager(this);
|
||||||
|
this.Dialog = DialogHelper;
|
||||||
|
this.ProgressWindow = ProgressWindowHelper;
|
||||||
|
this.ProgressWindow.setIconURI(
|
||||||
|
"default",
|
||||||
|
`chrome://${config.addonRef}/content/icons/favicon.png`
|
||||||
|
);
|
||||||
|
this.Clipboard = ClipboardHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterAll() {
|
||||||
|
unregister(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Addon;
|
30
src/api.ts
Normal file
30
src/api.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { getPref } from "./utils/prefs";
|
||||||
|
import { TranslateTask } from "./utils/translate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To plugin developers: Please use this API to translate your custom text.
|
||||||
|
*
|
||||||
|
* @param raw raw text for translation.
|
||||||
|
* @param service service id. See src/utils/config.ts > SERVICES
|
||||||
|
* @returns TranslateTask object.
|
||||||
|
*/
|
||||||
|
async function translate(raw: string, service?: string) {
|
||||||
|
const data: TranslateTask = {
|
||||||
|
id: `${Zotero.Utilities.randomString()}-${new Date().getTime()}`,
|
||||||
|
type: "custom",
|
||||||
|
raw,
|
||||||
|
result: "",
|
||||||
|
audio: [],
|
||||||
|
service: service || (getPref("translateSource") as string),
|
||||||
|
candidateServices: [],
|
||||||
|
itemId: -1,
|
||||||
|
status: "waiting",
|
||||||
|
extraTasks: [],
|
||||||
|
};
|
||||||
|
await addon.data.translate.services.runTranslationTask(data, {
|
||||||
|
noDisplay: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { translate };
|
220
src/hooks.ts
Normal file
220
src/hooks.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { initLocale } from "./utils/locale";
|
||||||
|
import {
|
||||||
|
registerPrefsScripts,
|
||||||
|
registerPrefsWindow,
|
||||||
|
} from "./modules/preferenceWindow";
|
||||||
|
import {
|
||||||
|
registerReaderTabPanel,
|
||||||
|
updateReaderTabPanels,
|
||||||
|
} from "./modules/tabpanel";
|
||||||
|
import { buildReaderPopup, updateReaderPopup } from "./modules/popup";
|
||||||
|
import { registerNotify } from "./modules/notify";
|
||||||
|
import {
|
||||||
|
checkReaderAnnotationButton,
|
||||||
|
registerReaderInitializer,
|
||||||
|
unregisterReaderInitializer,
|
||||||
|
} from "./modules/reader";
|
||||||
|
import { getPref, setPref } from "./utils/prefs";
|
||||||
|
import {
|
||||||
|
addTranslateAnnotationTask,
|
||||||
|
addTranslateTask,
|
||||||
|
addTranslateTitleTask,
|
||||||
|
getLastTranslateTask,
|
||||||
|
TranslateTask,
|
||||||
|
} from "./utils/translate";
|
||||||
|
import { setDefaultPrefSettings } from "./modules/defaultSettings";
|
||||||
|
import Addon from "./addon";
|
||||||
|
import { registerMenu } from "./modules/menu";
|
||||||
|
import {
|
||||||
|
registerExtraColumns,
|
||||||
|
registerTitleRenderer,
|
||||||
|
} from "./modules/itemTree";
|
||||||
|
import { registerShortcuts } from "./modules/shortcuts";
|
||||||
|
import { config } from "../package.json";
|
||||||
|
import { registerItemBoxExtraRows } from "./modules/itemBox";
|
||||||
|
import { registerPrompt } from "./modules/prompt";
|
||||||
|
|
||||||
|
async function onStartup() {
|
||||||
|
await Promise.all([
|
||||||
|
Zotero.initializationPromise,
|
||||||
|
Zotero.unlockPromise,
|
||||||
|
Zotero.uiReadyPromise,
|
||||||
|
]);
|
||||||
|
initLocale();
|
||||||
|
|
||||||
|
setDefaultPrefSettings();
|
||||||
|
|
||||||
|
registerNotify(["item"]);
|
||||||
|
registerReaderTabPanel();
|
||||||
|
registerReaderInitializer();
|
||||||
|
registerPrefsWindow();
|
||||||
|
registerMenu();
|
||||||
|
await registerExtraColumns();
|
||||||
|
await registerItemBoxExtraRows();
|
||||||
|
registerTitleRenderer();
|
||||||
|
registerShortcuts();
|
||||||
|
registerPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShutdown(): void {
|
||||||
|
ztoolkit.unregisterAll();
|
||||||
|
unregisterReaderInitializer();
|
||||||
|
// Remove addon object
|
||||||
|
addon.data.alive = false;
|
||||||
|
delete Zotero[config.addonInstance];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is just an example of dispatcher for Notify events.
|
||||||
|
* Any operations should be placed in a function to keep this function clear.
|
||||||
|
*/
|
||||||
|
function onNotify(
|
||||||
|
event: string,
|
||||||
|
type: string,
|
||||||
|
ids: Array<string | number>,
|
||||||
|
extraData: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
if (event === "add" && type === "item") {
|
||||||
|
const annotationItems = Zotero.Items.get(ids as number[]).filter((item) =>
|
||||||
|
item.isAnnotation()
|
||||||
|
);
|
||||||
|
if (annotationItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkReaderAnnotationButton(annotationItems);
|
||||||
|
if (getPref("enableComment")) {
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
annotationItems
|
||||||
|
.map((item) => addTranslateAnnotationTask(item.id))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrefsLoad(event: Event) {
|
||||||
|
registerPrefsScripts((event.target as any).ownerGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShortcuts(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case "library":
|
||||||
|
{
|
||||||
|
addon.hooks.onSwitchTitleColumnDisplay();
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
ZoteroPane.getSelectedItems(true)
|
||||||
|
.map((id) => addTranslateTitleTask(id, true))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "reader":
|
||||||
|
{
|
||||||
|
addon.hooks.onTranslate(undefined, {
|
||||||
|
noCheckZoteroItemLanguage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTranslate(): Promise<void>;
|
||||||
|
async function onTranslate(
|
||||||
|
options: Parameters<
|
||||||
|
Addon["data"]["translate"]["services"]["runTranslationTask"]
|
||||||
|
>["1"]
|
||||||
|
): Promise<void>;
|
||||||
|
async function onTranslate(
|
||||||
|
task: TranslateTask | undefined,
|
||||||
|
options?: Parameters<
|
||||||
|
Addon["data"]["translate"]["services"]["runTranslationTask"]
|
||||||
|
>["1"]
|
||||||
|
): Promise<void>;
|
||||||
|
async function onTranslate(...data: any) {
|
||||||
|
let task = undefined;
|
||||||
|
let options = {};
|
||||||
|
if (data.length === 1) {
|
||||||
|
if (data[0].raw) {
|
||||||
|
task = data[0];
|
||||||
|
} else {
|
||||||
|
options = data[0];
|
||||||
|
}
|
||||||
|
} else if (data.length === 2) {
|
||||||
|
task = data[0];
|
||||||
|
options = data[1];
|
||||||
|
}
|
||||||
|
await addon.data.translate.services.runTranslationTask(task, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTranslateInBatch(
|
||||||
|
tasks: TranslateTask[],
|
||||||
|
options: Parameters<
|
||||||
|
Addon["data"]["translate"]["services"]["runTranslationTask"]
|
||||||
|
>["1"] = {}
|
||||||
|
) {
|
||||||
|
for (const task of tasks) {
|
||||||
|
await addon.hooks.onTranslate(task, options);
|
||||||
|
await Zotero.Promise.delay(addon.data.translate.batchTaskDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReaderTextSelection(readerInstance: _ZoteroTypes.ReaderInstance) {
|
||||||
|
const selection = ztoolkit.Reader.getSelectedText(readerInstance);
|
||||||
|
const task = getLastTranslateTask();
|
||||||
|
if (task?.raw === selection) {
|
||||||
|
addon.hooks.onReaderPopupBuild(readerInstance);
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addTranslateTask(selection, readerInstance.itemID);
|
||||||
|
addon.hooks.onReaderPopupBuild(readerInstance);
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
if (getPref("enableAuto")) {
|
||||||
|
addon.hooks.onTranslate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReaderPopupBuild(readerInstance: _ZoteroTypes.ReaderInstance) {
|
||||||
|
buildReaderPopup(readerInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReaderPopupRefresh() {
|
||||||
|
updateReaderPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReaderTabPanelRefresh() {
|
||||||
|
updateReaderTabPanels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwitchTitleColumnDisplay() {
|
||||||
|
setPref(
|
||||||
|
"titleColumnMode",
|
||||||
|
getPref("titleColumnMode") === "raw" ? "result" : "raw"
|
||||||
|
);
|
||||||
|
ztoolkit.ItemTree.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add your hooks here. For element click, etc.
|
||||||
|
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
|
||||||
|
// Otherwise the code would be hard to read and maintain.
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onStartup,
|
||||||
|
onShutdown,
|
||||||
|
onNotify,
|
||||||
|
onPrefsLoad,
|
||||||
|
onShortcuts,
|
||||||
|
onTranslate,
|
||||||
|
onTranslateInBatch,
|
||||||
|
onReaderTextSelection,
|
||||||
|
onReaderPopupBuild,
|
||||||
|
onReaderPopupRefresh,
|
||||||
|
onReaderTabPanelRefresh,
|
||||||
|
onSwitchTitleColumnDisplay,
|
||||||
|
};
|
28
src/index.ts
Normal file
28
src/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { BasicTool } from "zotero-plugin-toolkit/dist/basic";
|
||||||
|
import Addon from "./addon";
|
||||||
|
import { config } from "../package.json";
|
||||||
|
|
||||||
|
const basicTool = new BasicTool();
|
||||||
|
|
||||||
|
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||||
|
// Set global variables
|
||||||
|
_globalThis.Zotero = basicTool.getGlobal("Zotero");
|
||||||
|
_globalThis.ZoteroPane = basicTool.getGlobal("ZoteroPane");
|
||||||
|
_globalThis.Zotero_Tabs = basicTool.getGlobal("Zotero_Tabs");
|
||||||
|
_globalThis.ZoteroContextPane = basicTool.getGlobal("ZoteroContextPane");
|
||||||
|
_globalThis.window = basicTool.getGlobal("window");
|
||||||
|
_globalThis.document = basicTool.getGlobal("document");
|
||||||
|
_globalThis.crypto = basicTool.getGlobal("crypto");
|
||||||
|
_globalThis.TextEncoder = basicTool.getGlobal("TextEncoder");
|
||||||
|
_globalThis.addon = new Addon();
|
||||||
|
_globalThis.ztoolkit = addon.data.ztoolkit;
|
||||||
|
ztoolkit.basicOptions.log.prefix = `[${config.addonName}]`;
|
||||||
|
ztoolkit.basicOptions.log.disableConsole = addon.data.env === "production";
|
||||||
|
ztoolkit.UI.basicOptions.ui.enableElementJSONLog =
|
||||||
|
addon.data.env === "development";
|
||||||
|
// Set the default false and enable manually
|
||||||
|
ztoolkit.UI.basicOptions.ui.enableElementRecord = false;
|
||||||
|
Zotero[config.addonInstance] = addon;
|
||||||
|
// Trigger addon hook for initialization
|
||||||
|
addon.hooks.onStartup();
|
||||||
|
}
|
67
src/modules/defaultSettings.ts
Normal file
67
src/modules/defaultSettings.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { getService, SERVICES } from "../utils/config";
|
||||||
|
import { clearPref, getPref, setPref } from "../utils/prefs";
|
||||||
|
import { setServiceSecret } from "../utils/translate";
|
||||||
|
|
||||||
|
export function setDefaultPrefSettings() {
|
||||||
|
const isZhCN = Zotero.locale === "zh-CN";
|
||||||
|
const servicesIds = SERVICES.map((service) => service.id);
|
||||||
|
if (!servicesIds.includes((getPref("translateSource") as string) || "")) {
|
||||||
|
// Google Translate is not accessible in China mainland
|
||||||
|
setPref("translateSource", isZhCN ? "cnki" : "googleapi");
|
||||||
|
}
|
||||||
|
if (!servicesIds.includes((getPref("dictSource") as string) || "")) {
|
||||||
|
setPref("dictSource", isZhCN ? "bingdict" : "freedictionaryapi");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getPref("targetLanguage")) {
|
||||||
|
setPref("targetLanguage", Zotero.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
const secrets = JSON.parse((getPref("secretObj") as string) || "{}");
|
||||||
|
for (const serviceId of servicesIds) {
|
||||||
|
if (typeof secrets[serviceId] === "undefined") {
|
||||||
|
secrets[serviceId] = getService(serviceId).defaultSecret || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPref("secretObj", JSON.stringify(secrets));
|
||||||
|
|
||||||
|
if (isZhCN && !getPref("disabledLanguages")) {
|
||||||
|
setPref("disabledLanguages", "zh,中文,中文;");
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraServices = getPref("extraEngines") as string;
|
||||||
|
if (extraServices.startsWith(",")) {
|
||||||
|
setPref("extraEngines", extraServices.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For NiuTrans login. niutransLog is deprecated.
|
||||||
|
const niutransApiKey = getPref("niutransApikey") as string;
|
||||||
|
if (niutransApiKey) {
|
||||||
|
setServiceSecret("niutranspro", niutransApiKey);
|
||||||
|
clearPref("niutransApikey");
|
||||||
|
}
|
||||||
|
if (getPref("translateSource") === "niutransLog") {
|
||||||
|
setPref("translateSource", "niutranspro");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const oldDict = JSON.parse(
|
||||||
|
(getPref("niutransDictLibList") as string) || "{}"
|
||||||
|
);
|
||||||
|
if (oldDict?.dlist) {
|
||||||
|
setPref("niutransDictLibList", JSON.stringify(oldDict.dlist));
|
||||||
|
} else {
|
||||||
|
setPref("niutransDictLibList", "[]");
|
||||||
|
}
|
||||||
|
const oldMemory = JSON.parse(
|
||||||
|
(getPref("niutransMemoryLibList") as string) || "{}"
|
||||||
|
);
|
||||||
|
if (oldMemory?.mlist) {
|
||||||
|
setPref("niutransMemoryLibList", JSON.stringify(oldMemory?.mlist));
|
||||||
|
} else {
|
||||||
|
setPref("niutransMemoryLibList", "[]");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setPref("niutransDictLibList", "[]");
|
||||||
|
setPref("niutransMemoryLibList", "[]");
|
||||||
|
}
|
||||||
|
}
|
42
src/modules/itemBox.ts
Normal file
42
src/modules/itemBox.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { getString } from "../utils/locale";
|
||||||
|
import { getPref } from "../utils/prefs";
|
||||||
|
|
||||||
|
export async function registerItemBoxExtraRows() {
|
||||||
|
if (getPref("showItemBoxTitleTranslation") !== false) {
|
||||||
|
await ztoolkit.ItemBox.register(
|
||||||
|
"titleTranslation",
|
||||||
|
getString("field.titleTranslation"),
|
||||||
|
// getField hook is registered in itemTree.ts
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
editable: false,
|
||||||
|
setFieldHook: (field, value, loadIn, item, original) => {
|
||||||
|
ztoolkit.ExtraField.setExtraField(item, field, value);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
index: 2,
|
||||||
|
multiline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getPref("showItemBoxAbstractTranslation") !== false) {
|
||||||
|
await ztoolkit.ItemBox.register(
|
||||||
|
"abstractTranslation",
|
||||||
|
getString("field.abstractTranslation"),
|
||||||
|
(field, unformatted, includeBaseMapped, item, original) => {
|
||||||
|
return ztoolkit.ExtraField.getExtraField(item, field) || "";
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editable: false,
|
||||||
|
setFieldHook: (field, value, loadIn, item, original) => {
|
||||||
|
ztoolkit.ExtraField.setExtraField(item, field, value);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
index: 3,
|
||||||
|
multiline: true,
|
||||||
|
collapsible: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
33
src/modules/itemTree.ts
Normal file
33
src/modules/itemTree.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { getString } from "../utils/locale";
|
||||||
|
import { getPref } from "../utils/prefs";
|
||||||
|
|
||||||
|
export async function registerExtraColumns() {
|
||||||
|
await ztoolkit.ItemTree.register(
|
||||||
|
"titleTranslation",
|
||||||
|
getString("field.titleTranslation"),
|
||||||
|
(
|
||||||
|
field: string,
|
||||||
|
unformatted: boolean,
|
||||||
|
includeBaseMapped: boolean,
|
||||||
|
item: Zotero.Item
|
||||||
|
) => {
|
||||||
|
return ztoolkit.ExtraField.getExtraField(item, field) || "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerTitleRenderer() {
|
||||||
|
ztoolkit.ItemTree.addRenderCellHook(
|
||||||
|
"title",
|
||||||
|
(index: number, data: string, column: any, original: Function) => {
|
||||||
|
if (getPref("titleColumnMode") === "result") {
|
||||||
|
const item = (ZoteroPane.itemsView.getRow(index) as any)
|
||||||
|
.ref as Zotero.Item;
|
||||||
|
data =
|
||||||
|
ztoolkit.ExtraField.getExtraField(item, "titleTranslation") || data;
|
||||||
|
}
|
||||||
|
const span = original(index, data, column) as HTMLSpanElement;
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
).then(() => ztoolkit.ItemTree.refresh());
|
||||||
|
}
|
81
src/modules/menu.ts
Normal file
81
src/modules/menu.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
import { getString } from "../utils/locale";
|
||||||
|
import {
|
||||||
|
addTranslateAbstractTask,
|
||||||
|
addTranslateTitleTask,
|
||||||
|
TranslateTask,
|
||||||
|
} from "../utils/translate";
|
||||||
|
|
||||||
|
export function registerMenu() {
|
||||||
|
const menuIcon = `chrome://${config.addonRef}/content/icons/favicon@0.5x.png`;
|
||||||
|
ztoolkit.Menu.register("item", {
|
||||||
|
tag: "menuseparator",
|
||||||
|
});
|
||||||
|
ztoolkit.Menu.register("item", {
|
||||||
|
tag: "menuitem",
|
||||||
|
label: getString("itemmenu.translateTitle.label"),
|
||||||
|
commandListener: (ev) => {
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
ZoteroPane.getSelectedItems(true)
|
||||||
|
.map((id) => addTranslateTitleTask(id, true))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: menuIcon,
|
||||||
|
});
|
||||||
|
ztoolkit.Menu.register("item", {
|
||||||
|
tag: "menuitem",
|
||||||
|
label: getString("itemmenu.translateAbstract.label"),
|
||||||
|
commandListener: (ev) => {
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
ZoteroPane.getSelectedItems(true)
|
||||||
|
.map((id) => addTranslateAbstractTask(id, true))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: menuIcon,
|
||||||
|
});
|
||||||
|
ztoolkit.Menu.register("item", {
|
||||||
|
tag: "menuitem",
|
||||||
|
label: `${getString("itemmenu.switchTitleMode.label")}(${getString(
|
||||||
|
"ctrl"
|
||||||
|
)} + T)`,
|
||||||
|
commandListener: (ev) => {
|
||||||
|
addon.hooks.onSwitchTitleColumnDisplay();
|
||||||
|
},
|
||||||
|
icon: menuIcon,
|
||||||
|
});
|
||||||
|
ztoolkit.Menu.register("item", {
|
||||||
|
tag: "menu",
|
||||||
|
label: getString("itemmenu.more.label"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "menuitem",
|
||||||
|
label: getString("itemmenu.retranslateTitle.label"),
|
||||||
|
commandListener: (ev) => {
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
ZoteroPane.getSelectedItems(true)
|
||||||
|
.map((id) => addTranslateTitleTask(id, false))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "menuitem",
|
||||||
|
label: getString("itemmenu.retranslateAbstract.label"),
|
||||||
|
commandListener: (ev) => {
|
||||||
|
addon.hooks.onTranslateInBatch(
|
||||||
|
ZoteroPane.getSelectedItems(true)
|
||||||
|
.map((id) => addTranslateAbstractTask(id, false))
|
||||||
|
.filter((task) => task) as TranslateTask[],
|
||||||
|
{ noDisplay: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: menuIcon,
|
||||||
|
});
|
||||||
|
}
|
27
src/modules/notify.ts
Normal file
27
src/modules/notify.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export function registerNotify(types: _ZoteroTypes.Notifier.Type[]) {
|
||||||
|
const callback = {
|
||||||
|
notify: async (...data: Parameters<_ZoteroTypes.Notifier.Notify>) => {
|
||||||
|
if (!addon?.data.alive) {
|
||||||
|
unregisterNotify(notifyID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addon.hooks.onNotify(...data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the callback in Zotero as an item observer
|
||||||
|
const notifyID = Zotero.Notifier.registerObserver(callback, types);
|
||||||
|
|
||||||
|
// Unregister callback when the window closes (important to avoid a memory leak)
|
||||||
|
window.addEventListener(
|
||||||
|
"unload",
|
||||||
|
(e: Event) => {
|
||||||
|
unregisterNotify(notifyID);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterNotify(notifyID: string) {
|
||||||
|
Zotero.Notifier.unregisterObserver(notifyID);
|
||||||
|
}
|
422
src/modules/popup.ts
Normal file
422
src/modules/popup.ts
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import { SVGIcon } from "../utils/config";
|
||||||
|
import { config } from "../../package.json";
|
||||||
|
import { getString } from "../utils/locale";
|
||||||
|
import { getPref, setPref } from "../utils/prefs";
|
||||||
|
import { addTranslateTask, getLastTranslateTask } from "../utils/translate";
|
||||||
|
import { slice } from "../utils/str";
|
||||||
|
|
||||||
|
export function updateReaderPopup() {
|
||||||
|
const popup = addon.data.popup.currentPopup;
|
||||||
|
if (!popup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enablePopup = getPref("enablePopup");
|
||||||
|
const hidePopupTextarea = getPref("enableHidePopupTextarea") as boolean;
|
||||||
|
Array.from(popup.querySelectorAll(`.${config.addonRef}-readerpopup`)).forEach(
|
||||||
|
(elem) => ((elem as HTMLElement).hidden = !enablePopup)
|
||||||
|
);
|
||||||
|
if (!enablePopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = getLastTranslateTask();
|
||||||
|
if (!task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popup.setAttribute("translate-task-id", task.id);
|
||||||
|
const idPrefix = popup?.getAttribute(`${config.addonRef}-prefix`);
|
||||||
|
const makeId = (type: string) => `${idPrefix}-${type}`;
|
||||||
|
|
||||||
|
const audiobox = popup?.querySelector(
|
||||||
|
`#${makeId("audiobox")}`
|
||||||
|
) as HTMLDivElement;
|
||||||
|
const translateButton = popup?.querySelector(
|
||||||
|
`#${makeId("translate")}`
|
||||||
|
) as HTMLDivElement;
|
||||||
|
const textarea = popup?.querySelector(
|
||||||
|
`#${makeId("text")}`
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
const addToNoteButton = popup?.querySelector(
|
||||||
|
`#${makeId("addtonote")}`
|
||||||
|
) as HTMLDivElement;
|
||||||
|
if (task.audio.length > 0 && getPref("showPlayBtn")) {
|
||||||
|
audiobox.innerHTML = "";
|
||||||
|
ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "fragment",
|
||||||
|
children: task.audio.map((audioData) => ({
|
||||||
|
tag: "button",
|
||||||
|
namespace: "html",
|
||||||
|
classList: ["toolbarButton"],
|
||||||
|
attributes: {
|
||||||
|
tabindex: "-1",
|
||||||
|
title: audioData.text,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: `🔊 ${audioData.text}`,
|
||||||
|
onclick: () => {
|
||||||
|
new (ztoolkit.getGlobal("Audio"))(audioData.url).play();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
styles: { whiteSpace: "nowrap", flexGrow: "1" },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
audiobox
|
||||||
|
);
|
||||||
|
}
|
||||||
|
translateButton.hidden = task.status !== "waiting";
|
||||||
|
textarea.hidden = hidePopupTextarea || task.status === "waiting";
|
||||||
|
textarea.value = task.result || task.raw;
|
||||||
|
textarea.style.fontSize = `${getPref("fontSize")}px`;
|
||||||
|
textarea.style.lineHeight = `${
|
||||||
|
Number(getPref("lineHeight")) * Number(getPref("fontSize"))
|
||||||
|
}px`;
|
||||||
|
addToNoteButton.hidden = !Boolean(ZoteroContextPane.getActiveEditor());
|
||||||
|
updatePopupSize(popup, textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReaderPopup(readerInstance: _ZoteroTypes.ReaderInstance) {
|
||||||
|
const popup = readerInstance._iframeWindow?.document.querySelector(
|
||||||
|
"#selection-menu"
|
||||||
|
) as HTMLDivElement;
|
||||||
|
if (!popup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addon.data.popup.currentPopup = popup;
|
||||||
|
popup.style.height = "-moz-fit-content";
|
||||||
|
popup.setAttribute(
|
||||||
|
`${config.addonRef}-prefix`,
|
||||||
|
`${config.addonRef}-${readerInstance._instanceID}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const colors = popup.querySelector(".colors") as HTMLDivElement;
|
||||||
|
colors.style.width = "100%";
|
||||||
|
colors.style.justifyContent = "space-evenly";
|
||||||
|
|
||||||
|
const keepSize = getPref("keepPopupSize") as boolean;
|
||||||
|
|
||||||
|
const makeId = (type: string) =>
|
||||||
|
`${config.addonRef}-${readerInstance._instanceID}-${type}`;
|
||||||
|
|
||||||
|
const onTextAreaCopy = getOnTextAreaCopy(popup, makeId("text"));
|
||||||
|
const hidePopupTextarea = getPref("enableHidePopupTextarea") as boolean;
|
||||||
|
|
||||||
|
ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "fragment",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
id: makeId("audiobox"),
|
||||||
|
classList: [`${config.addonRef}-readerpopup`],
|
||||||
|
styles: {
|
||||||
|
display: "flex",
|
||||||
|
width: "calc(100% - 4px)",
|
||||||
|
marginLeft: "2px",
|
||||||
|
justifyContent: "space-evenly",
|
||||||
|
},
|
||||||
|
ignoreIfExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
id: makeId("translate"),
|
||||||
|
classList: ["wide-button", `${config.addonRef}-readerpopup`],
|
||||||
|
properties: {
|
||||||
|
innerHTML: `${SVGIcon}${getString("readerpopup.translate.label")}`,
|
||||||
|
hidden: getPref("enableAuto"),
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "mouseup",
|
||||||
|
listener: (ev: Event) => {
|
||||||
|
addon.hooks.onTranslate({ noCheckZoteroItemLanguage: true });
|
||||||
|
const button = ev.target as HTMLDivElement;
|
||||||
|
button.hidden = true;
|
||||||
|
(
|
||||||
|
button.ownerDocument.querySelector(
|
||||||
|
`#${makeId("text")}`
|
||||||
|
) as HTMLTextAreaElement
|
||||||
|
).hidden = hidePopupTextarea;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ignoreIfExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "textarea",
|
||||||
|
id: makeId("text"),
|
||||||
|
attributes: {
|
||||||
|
rows: "3",
|
||||||
|
columns: "10",
|
||||||
|
},
|
||||||
|
classList: [
|
||||||
|
`${config.addonRef}-popup-textarea`,
|
||||||
|
`${config.addonRef}-readerpopup`,
|
||||||
|
],
|
||||||
|
styles: {
|
||||||
|
fontSize: `${getPref("fontSize")}px`,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
lineHeight: `${
|
||||||
|
Number(getPref("lineHeight")) * Number(getPref("fontSize"))
|
||||||
|
}px`,
|
||||||
|
width: keepSize ? `${getPref("popupWidth")}px` : "-moz-available",
|
||||||
|
height: `${Math.max(
|
||||||
|
keepSize ? Number(getPref("popupHeight")) : 30
|
||||||
|
)}px`,
|
||||||
|
marginLeft: "2px",
|
||||||
|
// @ts-ignore
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
onpointerup: (e: Event) => e.stopPropagation(),
|
||||||
|
ondragstart: (e: Event) => e.stopPropagation(),
|
||||||
|
spellcheck: false,
|
||||||
|
value: ztoolkit.Reader.getSelectedText(readerInstance),
|
||||||
|
},
|
||||||
|
ignoreIfExists: true,
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "mousedown",
|
||||||
|
listener: (_ev) => {
|
||||||
|
_ev.target?.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
onTextAreaResize as (ev: Event) => void
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mouseup",
|
||||||
|
listener: (_ev) => {
|
||||||
|
_ev.target?.removeEventListener(
|
||||||
|
"mousemove",
|
||||||
|
onTextAreaResize as (ev: Event) => void
|
||||||
|
);
|
||||||
|
const textarea = popup.querySelector(
|
||||||
|
`#${makeId("text")}`
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (popup.scrollWidth > textarea.offsetWidth + 4) {
|
||||||
|
textarea.style.width = `${popup.scrollWidth - 4}px`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mouseenter",
|
||||||
|
listener: (_ev) => {
|
||||||
|
_ev.target?.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
onTextAreaCopy as (ev: Event) => void
|
||||||
|
);
|
||||||
|
const head =
|
||||||
|
readerInstance._iframe.contentWindow.document.querySelector(
|
||||||
|
"head"
|
||||||
|
);
|
||||||
|
ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "style",
|
||||||
|
id: makeId("style"),
|
||||||
|
properties: {
|
||||||
|
innerHTML: `.${config.addonRef}-popup-textarea::-moz-selection {background: #7fbbea;}`,
|
||||||
|
},
|
||||||
|
skipIfExists: true,
|
||||||
|
},
|
||||||
|
head
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mouseleave",
|
||||||
|
listener: (_ev) => {
|
||||||
|
_ev.target?.removeEventListener(
|
||||||
|
"keydown",
|
||||||
|
onTextAreaCopy as (ev: Event) => void
|
||||||
|
);
|
||||||
|
const head =
|
||||||
|
readerInstance._iframe.contentWindow.document.querySelector(
|
||||||
|
"head"
|
||||||
|
);
|
||||||
|
ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "style",
|
||||||
|
id: makeId("style"),
|
||||||
|
properties: {
|
||||||
|
innerHTML: `.${config.addonRef}-popup-textarea::-moz-selection {background: #bfbfbf;}`,
|
||||||
|
},
|
||||||
|
skipIfExists: true,
|
||||||
|
},
|
||||||
|
head
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "dblclick",
|
||||||
|
listener: (_ev) => {
|
||||||
|
const textarea = popup.querySelector(
|
||||||
|
`#${makeId("text")}`
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = textarea.value.length;
|
||||||
|
const text = textarea.value.slice(
|
||||||
|
textarea.selectionStart,
|
||||||
|
textarea.selectionEnd
|
||||||
|
);
|
||||||
|
new ztoolkit.Clipboard().addText(text, "text/unicode").copy();
|
||||||
|
new ztoolkit.ProgressWindow("Copied to Clipboard")
|
||||||
|
.createLine({
|
||||||
|
text: slice(text, 50),
|
||||||
|
progress: 100,
|
||||||
|
type: "default",
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
id: makeId("addtonote"),
|
||||||
|
classList: ["wide-button", `${config.addonRef}-readerpopup`],
|
||||||
|
properties: {
|
||||||
|
innerHTML: `${SVGIcon}${Zotero.getString("pdfReader.addToNote")}`,
|
||||||
|
},
|
||||||
|
ignoreIfExists: true,
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "mouseup",
|
||||||
|
listener: async (ev) => {
|
||||||
|
const noteEditor =
|
||||||
|
ZoteroContextPane && ZoteroContextPane.getActiveEditor();
|
||||||
|
if (!noteEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editorInstance = noteEditor.getCurrentInstance();
|
||||||
|
if (!editorInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection =
|
||||||
|
ztoolkit.Reader.getSelectedText(readerInstance);
|
||||||
|
const task = addTranslateTask(
|
||||||
|
selection,
|
||||||
|
readerInstance.itemID,
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
if (!task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await addon.hooks.onTranslate(task, {
|
||||||
|
noCheckZoteroItemLanguage: true,
|
||||||
|
noDisplay: true,
|
||||||
|
});
|
||||||
|
if (task.status !== "success") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const replaceMode = getPref("enableNoteReplaceMode") as boolean;
|
||||||
|
const { html } =
|
||||||
|
Zotero.EditorInstanceUtilities.serializeAnnotations([
|
||||||
|
{
|
||||||
|
type: "highlight",
|
||||||
|
text: replaceMode ? task.result : task.raw,
|
||||||
|
comment: replaceMode ? "" : task.result,
|
||||||
|
attachmentItemID: task.itemId,
|
||||||
|
pageLabel:
|
||||||
|
// @ts-ignore
|
||||||
|
readerInstance._iframeWindow.wrappedJSObject.extractor
|
||||||
|
.pageLabelsCache[readerInstance.state.pageIndex],
|
||||||
|
position: {
|
||||||
|
rects: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
editorInstance._postMessage({
|
||||||
|
action: "insertHTML",
|
||||||
|
pos: null,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
popup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTextAreaResize(ev: MouseEvent) {
|
||||||
|
if (getPref("keepPopupSize")) {
|
||||||
|
const textarea = ev.target as HTMLTextAreaElement;
|
||||||
|
setPref("popupWidth", textarea.offsetWidth);
|
||||||
|
setPref("popupHeight", textarea.offsetHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOnTextAreaCopy(selectionMenu: HTMLElement, targetId: string) {
|
||||||
|
return (ev: KeyboardEvent) => {
|
||||||
|
const textarea = selectionMenu.querySelector(
|
||||||
|
`#${targetId}`
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
const isMod = ev.ctrlKey || ev.metaKey;
|
||||||
|
if (ev.key === "c" && isMod) {
|
||||||
|
ztoolkit.getGlobal("setTimeout")(() => {
|
||||||
|
new ztoolkit.Clipboard()
|
||||||
|
.addText(
|
||||||
|
textarea.value.slice(
|
||||||
|
textarea.selectionStart,
|
||||||
|
textarea.selectionEnd
|
||||||
|
),
|
||||||
|
"text/unicode"
|
||||||
|
)
|
||||||
|
.copy();
|
||||||
|
}, 10);
|
||||||
|
ev.stopPropagation();
|
||||||
|
} else if (ev.key === "a" && isMod) {
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = textarea.value.length;
|
||||||
|
ev.stopPropagation();
|
||||||
|
} else if (ev.key === "x" && isMod) {
|
||||||
|
new ztoolkit.Clipboard()
|
||||||
|
.addText(
|
||||||
|
textarea.value.slice(textarea.selectionStart, textarea.selectionEnd),
|
||||||
|
"text/unicode"
|
||||||
|
)
|
||||||
|
.copy();
|
||||||
|
textarea.value = `${textarea.value.slice(
|
||||||
|
0,
|
||||||
|
textarea.selectionStart
|
||||||
|
)}${textarea.value.slice(textarea.selectionEnd)}`;
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePopupSize(
|
||||||
|
selectionMenu: HTMLDivElement,
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
resetSize: boolean = true
|
||||||
|
): void {
|
||||||
|
const keepSize = getPref("keepPopupSize") as boolean;
|
||||||
|
if (keepSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resetSize) {
|
||||||
|
textarea.style.width = "-moz-available";
|
||||||
|
textarea.style.height = "30px";
|
||||||
|
}
|
||||||
|
const viewer = selectionMenu.ownerDocument.querySelector(
|
||||||
|
"#viewer"
|
||||||
|
) as HTMLDivElement;
|
||||||
|
// Get current H & W
|
||||||
|
let textHeight = textarea.scrollHeight;
|
||||||
|
let textWidth = textarea.scrollWidth;
|
||||||
|
const newWidth = textWidth + 20;
|
||||||
|
// Check until H/W<0.75 and don't overflow viewer border
|
||||||
|
if (
|
||||||
|
textHeight / textWidth > 0.75 &&
|
||||||
|
selectionMenu.offsetLeft + newWidth < viewer.offsetWidth
|
||||||
|
) {
|
||||||
|
// Update width
|
||||||
|
textarea.style.width = `${newWidth}px`;
|
||||||
|
updatePopupSize(selectionMenu, textarea, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update height
|
||||||
|
textarea.style.height = `${textHeight + 3}px`;
|
||||||
|
}
|
438
src/modules/preferenceWindow.ts
Normal file
438
src/modules/preferenceWindow.ts
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
import { getService, LANG_CODE, SERVICES } from "../utils/config";
|
||||||
|
import { getString } from "../utils/locale";
|
||||||
|
import { getPref, setPref } from "../utils/prefs";
|
||||||
|
import {
|
||||||
|
validateServiceSecret,
|
||||||
|
secretStatusButtonData,
|
||||||
|
setServiceSecret,
|
||||||
|
} from "../utils/translate";
|
||||||
|
|
||||||
|
export function registerPrefsWindow() {
|
||||||
|
ztoolkit.PreferencePane.register({
|
||||||
|
pluginID: config.addonID,
|
||||||
|
src: rootURI + "chrome/content/preferences.xhtml",
|
||||||
|
label: getString("pref.title"),
|
||||||
|
image: `chrome://${config.addonRef}/content/icons/favicon.png`,
|
||||||
|
extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
|
||||||
|
defaultXUL: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPrefsScripts(_window: Window) {
|
||||||
|
// This function is called when the prefs window is opened
|
||||||
|
addon.data.prefs.window = _window;
|
||||||
|
buildPrefsPane();
|
||||||
|
updatePrefsPaneDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrefsPane() {
|
||||||
|
const doc = addon.data.prefs.window?.document;
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// menus
|
||||||
|
ztoolkit.UI.replaceElement(
|
||||||
|
{
|
||||||
|
tag: "menulist",
|
||||||
|
id: makeId("sentenceServices"),
|
||||||
|
attributes: {
|
||||||
|
value: getPref("translateSource") as string,
|
||||||
|
native: "true",
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
listener: (e: Event) => {
|
||||||
|
onPrefsEvents("setSentenceService");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "menupopup",
|
||||||
|
children: SERVICES.filter(
|
||||||
|
(service) => service.type === "sentence"
|
||||||
|
).map((service) => ({
|
||||||
|
tag: "menuitem",
|
||||||
|
attributes: {
|
||||||
|
label: getString(`service.${service.id}`),
|
||||||
|
value: service.id,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
doc.querySelector(`#${makeId("sentenceServices-placeholder")}`)!
|
||||||
|
);
|
||||||
|
|
||||||
|
ztoolkit.UI.replaceElement(
|
||||||
|
{
|
||||||
|
tag: "menulist",
|
||||||
|
id: makeId("wordServices"),
|
||||||
|
attributes: {
|
||||||
|
value: getPref("dictSource") as string,
|
||||||
|
native: "true",
|
||||||
|
},
|
||||||
|
classList: ["use-word-service"],
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
listener: (e: Event) => {
|
||||||
|
onPrefsEvents("setWordService");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "menupopup",
|
||||||
|
children: SERVICES.filter((service) => service.type === "word").map(
|
||||||
|
(service) => ({
|
||||||
|
tag: "menuitem",
|
||||||
|
attributes: {
|
||||||
|
label: getString(`service.${service.id}`),
|
||||||
|
value: service.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
doc.querySelector(`#${makeId("wordServices-placeholder")}`)!
|
||||||
|
);
|
||||||
|
|
||||||
|
ztoolkit.UI.replaceElement(
|
||||||
|
{
|
||||||
|
tag: "menulist",
|
||||||
|
id: makeId("langfrom"),
|
||||||
|
attributes: {
|
||||||
|
value: getPref("sourceLanguage") as string,
|
||||||
|
native: "true",
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
listener: (e: Event) => {
|
||||||
|
onPrefsEvents("setSourceLanguage");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styles: {
|
||||||
|
maxWidth: "250px",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "menupopup",
|
||||||
|
children: LANG_CODE.map((lang) => ({
|
||||||
|
tag: "menuitem",
|
||||||
|
attributes: {
|
||||||
|
label: lang.name,
|
||||||
|
value: lang.code,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
doc.querySelector(`#${makeId("langfrom-placeholder")}`)!
|
||||||
|
);
|
||||||
|
|
||||||
|
ztoolkit.UI.replaceElement(
|
||||||
|
{
|
||||||
|
tag: "menulist",
|
||||||
|
id: makeId("langto"),
|
||||||
|
attributes: {
|
||||||
|
value: getPref("targetLanguage") as string,
|
||||||
|
native: "true",
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
listener: (e: Event) => {
|
||||||
|
onPrefsEvents("setTargetLanguage");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styles: {
|
||||||
|
maxWidth: "250px",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "menupopup",
|
||||||
|
children: LANG_CODE.map((lang) => ({
|
||||||
|
tag: "menuitem",
|
||||||
|
attributes: {
|
||||||
|
label: lang.name,
|
||||||
|
value: lang.code,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
doc.querySelector(`#${makeId("langto-placeholder")}`)!
|
||||||
|
);
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("enableAuto")}`)
|
||||||
|
?.addEventListener("command", (e: Event) => {
|
||||||
|
onPrefsEvents("setAutoTranslateSelection");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("enableComment")}`)
|
||||||
|
?.addEventListener("command", (e: Event) => {
|
||||||
|
onPrefsEvents("setAutoTranslateAnnotation");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("enablePopup")}`)
|
||||||
|
?.addEventListener("command", (e: Event) => {
|
||||||
|
onPrefsEvents("setEnablePopup");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("enableAddToNote")}`)
|
||||||
|
?.addEventListener("command", (e: Event) => {
|
||||||
|
onPrefsEvents("setEnableAddToNote");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("useWordService")}`)
|
||||||
|
?.addEventListener("command", (e: Event) => {
|
||||||
|
onPrefsEvents("setUseWordService");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("sentenceServicesSecret")}`)
|
||||||
|
?.addEventListener("input", (e: Event) => {
|
||||||
|
onPrefsEvents("updateSentenceSecret");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("wordServicesSecret")}`)
|
||||||
|
?.addEventListener("input", (e: Event) => {
|
||||||
|
onPrefsEvents("updateWordSecret");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("fontSize")}`)
|
||||||
|
?.addEventListener("input", (e: Event) => {
|
||||||
|
onPrefsEvents("updateFontSize");
|
||||||
|
});
|
||||||
|
|
||||||
|
doc
|
||||||
|
.querySelector(`#${makeId("lineHeight")}`)
|
||||||
|
?.addEventListener("input", (e: Event) => {
|
||||||
|
onPrefsEvents("updatelineHeight");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePrefsPaneDefault() {
|
||||||
|
onPrefsEvents("setAutoTranslateAnnotation", false);
|
||||||
|
onPrefsEvents("setEnablePopup", false);
|
||||||
|
onPrefsEvents("setUseWordService", false);
|
||||||
|
onPrefsEvents("setSentenceSecret", false);
|
||||||
|
onPrefsEvents("setWordSecret", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrefsEvents(type: string, fromElement: boolean = true) {
|
||||||
|
const doc = addon.data.prefs.window?.document;
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDisabled = (className: string, disabled: boolean) => {
|
||||||
|
doc
|
||||||
|
.querySelectorAll(`.${className}`)
|
||||||
|
.forEach(
|
||||||
|
(elem) => ((elem as XUL.Element & XUL.IDisabled).disabled = disabled)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
switch (type) {
|
||||||
|
case "setAutoTranslateSelection":
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
break;
|
||||||
|
case "setAutoTranslateAnnotation":
|
||||||
|
{
|
||||||
|
let elemValue = fromElement
|
||||||
|
? (doc.querySelector(`#${makeId("enableComment")}`) as XUL.Checkbox)
|
||||||
|
.checked
|
||||||
|
: (getPref("enableComment") as boolean);
|
||||||
|
const hidden = !elemValue;
|
||||||
|
setDisabled("auto-annotation", hidden);
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setEnablePopup":
|
||||||
|
{
|
||||||
|
let elemValue = fromElement
|
||||||
|
? (doc.querySelector(`#${makeId("enablePopup")}`) as XUL.Checkbox)
|
||||||
|
.checked
|
||||||
|
: (getPref("enablePopup") as boolean);
|
||||||
|
const hidden = !elemValue;
|
||||||
|
setDisabled("enable-popup", hidden);
|
||||||
|
if (!hidden) {
|
||||||
|
onPrefsEvents("setEnableAddToNote", fromElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setEnableAddToNote":
|
||||||
|
{
|
||||||
|
let elemValue = fromElement
|
||||||
|
? (doc.querySelector(`#${makeId("enableAddToNote")}`) as XUL.Checkbox)
|
||||||
|
.checked
|
||||||
|
: (getPref("enableNote") as boolean);
|
||||||
|
const hidden = !elemValue;
|
||||||
|
setDisabled("enable-popup-addtonote", hidden);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setUseWordService":
|
||||||
|
{
|
||||||
|
let elemValue = fromElement
|
||||||
|
? (doc.querySelector(`#${makeId("useWordService")}`) as XUL.Checkbox)
|
||||||
|
.checked
|
||||||
|
: (getPref("enableDict") as boolean);
|
||||||
|
const hidden = !elemValue;
|
||||||
|
setDisabled("use-word-service", hidden);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setSentenceService":
|
||||||
|
{
|
||||||
|
setPref(
|
||||||
|
"translateSource",
|
||||||
|
(
|
||||||
|
doc.querySelector(`#${makeId("sentenceServices")}`) as XUL.MenuList
|
||||||
|
).getAttribute("value")!
|
||||||
|
);
|
||||||
|
onPrefsEvents("setSentenceSecret", fromElement);
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "updateSentenceSecret":
|
||||||
|
{
|
||||||
|
setServiceSecret(
|
||||||
|
getPref("translateSource") as string,
|
||||||
|
(
|
||||||
|
doc.querySelector(
|
||||||
|
`#${makeId("sentenceServicesSecret")}`
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setSentenceSecret":
|
||||||
|
{
|
||||||
|
const serviceId = getPref("translateSource") as string;
|
||||||
|
const secretCheckResult = validateServiceSecret(
|
||||||
|
serviceId,
|
||||||
|
(validateResult) => {
|
||||||
|
if (fromElement && !validateResult.status) {
|
||||||
|
addon.data.prefs.window?.alert(
|
||||||
|
`You see this because the translation service ${serviceId} requires SECRET, which is NOT correctly set.\n\nDetails:\n${validateResult.info}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(
|
||||||
|
doc.querySelector(
|
||||||
|
`#${makeId("sentenceServicesSecret")}`
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value = secretCheckResult.secret;
|
||||||
|
// Update secret status button
|
||||||
|
const statusButtonData = secretStatusButtonData[serviceId];
|
||||||
|
const statusButton = doc.querySelector(
|
||||||
|
`#${makeId("sentenceServicesStatus")}`
|
||||||
|
) as XUL.Button;
|
||||||
|
if (statusButtonData) {
|
||||||
|
statusButton.hidden = false;
|
||||||
|
statusButton.label = getString(
|
||||||
|
statusButtonData.labels[secretCheckResult.status ? "pass" : "fail"]
|
||||||
|
);
|
||||||
|
statusButton.onclick = (ev) => {
|
||||||
|
statusButtonData.callback(secretCheckResult.status);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
statusButton.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setWordService":
|
||||||
|
{
|
||||||
|
setPref(
|
||||||
|
"dictSource",
|
||||||
|
(
|
||||||
|
doc.querySelector(`#${makeId("wordServices")}`) as XUL.MenuList
|
||||||
|
).getAttribute("value")!
|
||||||
|
);
|
||||||
|
onPrefsEvents("setWordSecret", fromElement);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "updateWordSecret":
|
||||||
|
{
|
||||||
|
setServiceSecret(
|
||||||
|
getPref("dictSource") as string,
|
||||||
|
(
|
||||||
|
doc.querySelector(
|
||||||
|
`#${makeId("wordServicesSecret")}`
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setWordSecret":
|
||||||
|
{
|
||||||
|
const serviceId = getPref("dictSource") as string;
|
||||||
|
const secretCheckResult = validateServiceSecret(
|
||||||
|
serviceId,
|
||||||
|
(validateResult) => {
|
||||||
|
if (fromElement && !validateResult.status) {
|
||||||
|
addon.data.prefs.window?.alert(
|
||||||
|
`You see this because the translation service ${serviceId} requires SECRET, which is NOT correctly set.\n\nDetails:\n${validateResult.info}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(
|
||||||
|
doc.querySelector(
|
||||||
|
`#${makeId("wordServicesSecret")}`
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value = secretCheckResult.secret;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setSourceLanguage":
|
||||||
|
{
|
||||||
|
setPref(
|
||||||
|
"sourceLanguage",
|
||||||
|
(
|
||||||
|
doc.querySelector(`#${makeId("langfrom")}`) as XUL.MenuList
|
||||||
|
).getAttribute("value")!
|
||||||
|
);
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "setTargetLanguage":
|
||||||
|
{
|
||||||
|
setPref(
|
||||||
|
"targetLanguage",
|
||||||
|
(
|
||||||
|
doc.querySelector(`#${makeId("langto")}`) as XUL.MenuList
|
||||||
|
).getAttribute("value")!
|
||||||
|
);
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "updateFontSize":
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
break;
|
||||||
|
case "updatelineHeight":
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeId(type: string) {
|
||||||
|
return `${config.addonRef}-${type}`;
|
||||||
|
}
|
227
src/modules/prompt.ts
Normal file
227
src/modules/prompt.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
|
||||||
|
export function registerPrompt() {
|
||||||
|
let getSelection = () => {
|
||||||
|
return ztoolkit.Reader.getSelectedText(
|
||||||
|
Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ztoolkit.Prompt.register([{
|
||||||
|
name: "Translate Sentences",
|
||||||
|
label: config.addonInstance,
|
||||||
|
when: () => {
|
||||||
|
const selection = getSelection();
|
||||||
|
const sl = Zotero.Prefs.get("ZoteroPDFTranslate.sourceLanguage") as string
|
||||||
|
const tl = Zotero.Prefs.get("ZoteroPDFTranslate.targetLanguage") as string
|
||||||
|
return selection.length > 0 && Zotero?.PDFTranslate && sl.startsWith("en") && tl.startsWith("zh")
|
||||||
|
},
|
||||||
|
callback: async (prompt) => {
|
||||||
|
const selection = getSelection();
|
||||||
|
const queue = Zotero.PDFTranslate.data.translate.queue
|
||||||
|
let task = queue.find((task: any) => task.raw == selection && task.result.length > 0)
|
||||||
|
task = null
|
||||||
|
if (!task) {
|
||||||
|
prompt.showTip("Loading...")
|
||||||
|
task = await Zotero.PDFTranslate.api.translate(selection)
|
||||||
|
Zotero.PDFTranslate.data.translate.queue.push(task)
|
||||||
|
// @ts-ignore
|
||||||
|
prompt.exit()
|
||||||
|
}
|
||||||
|
prompt.inputNode.placeholder = task.service
|
||||||
|
const rawText = task.raw, resultText = task.result;
|
||||||
|
let addSentences = (node: HTMLElement, text: string, dividers: string[]) => {
|
||||||
|
let i = 0
|
||||||
|
let sentences: string[] = []
|
||||||
|
let sentence = ""
|
||||||
|
// https://www.npmjs.com/package/sentence-extractor?activeTab=explore
|
||||||
|
const abbrs = ["a.m.", "p.m.", "vol.", "inc.", "jr.", "dr.", "tex.", "co.", "prof.", "rev.", "revd.", "hon.", "v.s.", "i.e.", "ie.",
|
||||||
|
"eg.", "e.g.", "al.", "st.", "ph.d.", "capt.", "mr.", "mrs.", "ms.", "fig."]
|
||||||
|
let getWord = (i: number) => {
|
||||||
|
let before, after;
|
||||||
|
before = text.slice(0, i).match(/[\.a-zA-Z]+$/)
|
||||||
|
after = text.slice(i + 1).match(/^[\.a-zA-Z]+/)
|
||||||
|
let word = ([before, ["."], after].filter(i => i) as string[][])
|
||||||
|
.map((i: string[]) => i[0]).join("")
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
let isAbbr = (i: number) => {
|
||||||
|
const word = getWord(i).toLowerCase().replace(/\s+/g, " ")
|
||||||
|
return abbrs.find((abbr: string) => {
|
||||||
|
abbr = abbr.toLowerCase()
|
||||||
|
return word == abbr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let isPotentialAbbr = (i: number) => {
|
||||||
|
const word = getWord(i)
|
||||||
|
let parts = word.split(".").filter(i => i)
|
||||||
|
return parts.length > 2 && parts.every(part => part.length <= 2)
|
||||||
|
}
|
||||||
|
while (i < text.length) {
|
||||||
|
let char = text[i]
|
||||||
|
sentence += char
|
||||||
|
if (dividers.indexOf(char) != -1) {
|
||||||
|
if (char == ".") {
|
||||||
|
if (
|
||||||
|
(i + 1 < text.length && text[i + 1] != " ") ||
|
||||||
|
(
|
||||||
|
(isAbbr(i) || isPotentialAbbr(i))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const blank = " "
|
||||||
|
i += 1
|
||||||
|
while (text[i] == blank) {
|
||||||
|
sentence += blank
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
sentences.push(sentence)
|
||||||
|
sentence = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
for (let i = 0; i < sentences.length; i++) {
|
||||||
|
const span = ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "span",
|
||||||
|
id: `sentence-${i}`,
|
||||||
|
properties: {
|
||||||
|
innerText: sentences[i]
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
borderRadius: "3px"
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "mousemove",
|
||||||
|
listener: () => {
|
||||||
|
const highlightColor = "#fee972";
|
||||||
|
|
||||||
|
let twinNode = [...container.querySelectorAll(".text-container")]
|
||||||
|
.find(e => e != node) as HTMLDivElement;
|
||||||
|
|
||||||
|
node.querySelectorAll("span").forEach(e => e.style.backgroundColor = "")
|
||||||
|
span.style.backgroundColor = highlightColor
|
||||||
|
|
||||||
|
twinNode?.querySelectorAll("span").forEach(e => e.style.backgroundColor = "");
|
||||||
|
|
||||||
|
const twinSpan = twinNode.querySelector(`span[id=sentence-${i}]`) as HTMLSpanElement
|
||||||
|
|
||||||
|
twinSpan.style.backgroundColor = highlightColor;
|
||||||
|
|
||||||
|
const twinNodeContainer = twinNode.parentNode as HTMLDivElement;
|
||||||
|
const nodeContainer = node.parentNode as HTMLDivElement;
|
||||||
|
if (direction == "column" && twinNode.classList.contains("result")) {
|
||||||
|
twinNodeContainer.scrollTo(0, twinSpan.offsetTop - twinNodeContainer.offsetHeight * .5 - nodeContainer.offsetHeight);
|
||||||
|
} else {
|
||||||
|
twinNodeContainer.scrollTo(0, twinSpan.offsetTop - twinNodeContainer.offsetHeight * .5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
node
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container = prompt.createCommandsContainer() as HTMLDivElement
|
||||||
|
// TODO: prefs: direction
|
||||||
|
const directions = ["row", "column"]
|
||||||
|
const direction = directions[1]
|
||||||
|
container.setAttribute("style", `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: ${direction};
|
||||||
|
padding: .5em 1em;
|
||||||
|
margin-left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 25em;
|
||||||
|
`)
|
||||||
|
const subContainers: HTMLDivElement[] = [];
|
||||||
|
[
|
||||||
|
["raw", rawText, [".", "?", "!"]],
|
||||||
|
["result", resultText, ["?", "!", "!", "。", "?"]]
|
||||||
|
].forEach((args: any[]) => {
|
||||||
|
let [className, text, dividers] = args;
|
||||||
|
const subContainer = ztoolkit.UI.createElement(document, "div", {
|
||||||
|
styles: {
|
||||||
|
padding: ".5em",
|
||||||
|
border: "1px solid #eee",
|
||||||
|
overflowY: "auto",
|
||||||
|
minWidth: "10em",
|
||||||
|
minHeight: "5em",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "justify",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
classList: [className, "text-container"],
|
||||||
|
styles: {
|
||||||
|
fontSize: "1em",
|
||||||
|
lineHeight: "1.5em",
|
||||||
|
marginBottom: ".5em"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
addSentences(subContainer.querySelector(".text-container")!, text, dividers);
|
||||||
|
subContainers.push(subContainer);
|
||||||
|
})
|
||||||
|
|
||||||
|
const size = 5
|
||||||
|
const resizer = ztoolkit.UI.createElement(document, "div", {
|
||||||
|
styles: {
|
||||||
|
height: (direction == "row" ? "100%" : `${size}px`),
|
||||||
|
width: (direction == "column" ? "100%" : `${size}px`),
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
cursor: direction == "column" ? "ns-resize" : "ew-resize",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let y = 0, x = 0;
|
||||||
|
let h = 0, w = 0;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const H = rect.height;
|
||||||
|
const W = rect.width;
|
||||||
|
const mouseDownHandler = function (e: MouseEvent) {
|
||||||
|
// hide
|
||||||
|
subContainers.forEach(div => {
|
||||||
|
div.querySelectorAll("span").forEach((e: HTMLSpanElement) => e.style.display = "none")
|
||||||
|
})
|
||||||
|
y = e.clientY;
|
||||||
|
x = e.clientX;
|
||||||
|
const rect = subContainers[1].getBoundingClientRect()
|
||||||
|
h = rect.height;
|
||||||
|
w = rect.width;
|
||||||
|
document.addEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.addEventListener('mouseup', mouseUpHandler);
|
||||||
|
};
|
||||||
|
const mouseMoveHandler = function (e: MouseEvent) {
|
||||||
|
const dy = e.clientY - y;
|
||||||
|
const dx = e.clientX - x;
|
||||||
|
if (direction == "column") {
|
||||||
|
subContainers[1].style.height = `${h - dy}px`;
|
||||||
|
subContainers[0].style.height = `${H - (h - dy) - size}px`;
|
||||||
|
}
|
||||||
|
if (direction == "row") {
|
||||||
|
subContainers[1].style.width = `${w - dx}px`;
|
||||||
|
subContainers[0].style.width = `${W - (w - dx) - size}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mouseUpHandler = function () {
|
||||||
|
// show
|
||||||
|
subContainers.forEach(div => {
|
||||||
|
div.querySelectorAll("span").forEach((e: HTMLSpanElement) => e.style.display = "")
|
||||||
|
})
|
||||||
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.removeEventListener('mouseup', mouseUpHandler);
|
||||||
|
};
|
||||||
|
resizer.addEventListener('mousedown', mouseDownHandler);
|
||||||
|
|
||||||
|
container.append(subContainers[0], resizer, subContainers[1])
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
}
|
184
src/modules/reader.ts
Normal file
184
src/modules/reader.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
import { SVGIcon } from "../utils/config";
|
||||||
|
import { addTranslateAnnotationTask } from "../utils/translate";
|
||||||
|
|
||||||
|
export function registerReaderInitializer() {
|
||||||
|
ztoolkit.ReaderInstance.register(
|
||||||
|
"initialized",
|
||||||
|
`${config.addonRef}-selection`,
|
||||||
|
initializeReaderSelectionEvent
|
||||||
|
);
|
||||||
|
ztoolkit.ReaderInstance.register(
|
||||||
|
"initialized",
|
||||||
|
`${config.addonRef}-annotationButtons`,
|
||||||
|
initializeReaderAnnotationButton
|
||||||
|
);
|
||||||
|
// Force re-initialize
|
||||||
|
Zotero.Reader._readers.forEach((r) => {
|
||||||
|
initializeReaderSelectionEvent(r);
|
||||||
|
initializeReaderAnnotationButton(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterReaderInitializer() {
|
||||||
|
Zotero.Reader._readers.forEach((r) => {
|
||||||
|
unInitializeReaderAnnotationButton(r);
|
||||||
|
unInitializeReaderSelectionEvent(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkReaderAnnotationButton(items: Zotero.Item[]) {
|
||||||
|
const hitSet = new Set<number>();
|
||||||
|
let t = 0;
|
||||||
|
const period = 100;
|
||||||
|
const wait = 5000;
|
||||||
|
while (items.length > hitSet.size && t < wait) {
|
||||||
|
for (const instance of Zotero.Reader._readers) {
|
||||||
|
const hitItems = await initializeReaderAnnotationButton(instance);
|
||||||
|
hitItems.forEach((item) => hitSet.add(item.id));
|
||||||
|
}
|
||||||
|
await Zotero.Promise.delay(period);
|
||||||
|
t += period;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeReaderSelectionEvent(
|
||||||
|
instance: _ZoteroTypes.ReaderInstance
|
||||||
|
) {
|
||||||
|
await instance._initPromise;
|
||||||
|
await instance._waitForReader();
|
||||||
|
if (instance._pdftranslateInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance._pdftranslateInitialized = true;
|
||||||
|
function selectionCallback(ev: MouseEvent) {
|
||||||
|
// Work around to only allow event from iframe#viewer
|
||||||
|
const target = ev.target as Element;
|
||||||
|
if (!target?.ownerDocument?.querySelector("#viewer")?.contains(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Callback when the selected content is not null
|
||||||
|
if (!ztoolkit.Reader.getSelectedText(instance)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
addon.data.translate.concatKey = ev.altKey;
|
||||||
|
addon.hooks.onReaderTextSelection(instance);
|
||||||
|
}
|
||||||
|
instance._iframeWindow?.addEventListener("pointerup", selectionCallback);
|
||||||
|
instance._pdftranslateSelectionCallback = selectionCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unInitializeReaderSelectionEvent(
|
||||||
|
instance: _ZoteroTypes.ReaderInstance
|
||||||
|
): Promise<void> {
|
||||||
|
await instance._initPromise;
|
||||||
|
await instance._waitForReader();
|
||||||
|
if (!instance._pdftranslateInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance._iframeWindow?.removeEventListener(
|
||||||
|
"pointerup",
|
||||||
|
instance._pdftranslateSelectionCallback
|
||||||
|
);
|
||||||
|
instance._pdftranslateInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeReaderAnnotationButton(
|
||||||
|
instance: _ZoteroTypes.ReaderInstance
|
||||||
|
): Promise<Zotero.Item[]> {
|
||||||
|
if (!instance) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await instance._initPromise;
|
||||||
|
await instance._waitForReader();
|
||||||
|
const _document = instance._iframeWindow?.document;
|
||||||
|
if (!_document) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const hitItems: Zotero.Item[] = [];
|
||||||
|
for (const moreButton of _document.querySelectorAll(".more")) {
|
||||||
|
if (moreButton.getAttribute("_pdftranslateInitialized") === "true") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
moreButton.setAttribute("_pdftranslateInitialized", "true");
|
||||||
|
|
||||||
|
let annotationWrapper = moreButton;
|
||||||
|
while (!annotationWrapper.getAttribute("data-sidebar-annotation-id")) {
|
||||||
|
annotationWrapper = annotationWrapper.parentElement!;
|
||||||
|
}
|
||||||
|
const itemKey =
|
||||||
|
annotationWrapper.getAttribute("data-sidebar-annotation-id") || "";
|
||||||
|
if (!instance.itemID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const libraryID = Zotero.Items.get(instance.itemID).libraryID;
|
||||||
|
const annotationItem = (await Zotero.Items.getByLibraryAndKeyAsync(
|
||||||
|
libraryID,
|
||||||
|
itemKey
|
||||||
|
)) as Zotero.Item;
|
||||||
|
|
||||||
|
if (!annotationItem) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hitItems.push(annotationItem);
|
||||||
|
|
||||||
|
ztoolkit.UI.insertElementBefore(
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
classList: ["icon"],
|
||||||
|
properties: {
|
||||||
|
innerHTML: SVGIcon,
|
||||||
|
},
|
||||||
|
listeners: [
|
||||||
|
{
|
||||||
|
type: "click",
|
||||||
|
listener: (e) => {
|
||||||
|
const task = addTranslateAnnotationTask(annotationItem.id);
|
||||||
|
addon.hooks.onTranslate(task, {
|
||||||
|
noCheckZoteroItemLanguage: true,
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mouseover",
|
||||||
|
listener: (e) => {
|
||||||
|
(e.target as HTMLElement).style.backgroundColor = "#F0F0F0";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mouseout",
|
||||||
|
listener: (e) => {
|
||||||
|
(e.target as HTMLElement).style.removeProperty(
|
||||||
|
"background-color"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enableElementRecord: true,
|
||||||
|
},
|
||||||
|
moreButton
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return hitItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unInitializeReaderAnnotationButton(
|
||||||
|
instance: _ZoteroTypes.ReaderInstance
|
||||||
|
): Promise<void> {
|
||||||
|
if (!instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await instance._initPromise;
|
||||||
|
await instance._waitForReader();
|
||||||
|
const _document = instance._iframeWindow?.document;
|
||||||
|
if (!_document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const moreButton of _document.querySelectorAll(".more")) {
|
||||||
|
if (moreButton.getAttribute("_pdftranslateInitialized") === "true") {
|
||||||
|
moreButton.removeAttribute("_pdftranslateInitialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
228
src/modules/services.ts
Normal file
228
src/modules/services.ts
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { getPref } from "../utils/prefs";
|
||||||
|
import {
|
||||||
|
getLastTranslateTask,
|
||||||
|
TranslateTask,
|
||||||
|
TranslateTaskRunner,
|
||||||
|
} from "../utils/translate";
|
||||||
|
|
||||||
|
export class TranslationServices {
|
||||||
|
[key: string]: TranslateTaskRunner | Function;
|
||||||
|
constructor() {
|
||||||
|
import("./services/baidu").then(
|
||||||
|
(e) => (this.baidu = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/baidufield").then(
|
||||||
|
(e) => (this.baidufield = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/bingdict").then(
|
||||||
|
(e) => (this.bingdict = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/caiyun").then(
|
||||||
|
(e) => (this.caiyun = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/cnki").then(
|
||||||
|
(e) => (this.cnki = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/collinsdict").then(
|
||||||
|
(e) => (this.collinsdict = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/deepl").then((e) => {
|
||||||
|
this.deeplfree = new TranslateTaskRunner(e.deeplfree);
|
||||||
|
this.deeplpro = new TranslateTaskRunner(e.deeplpro);
|
||||||
|
});
|
||||||
|
import("./services/deeplx").then((e) => {
|
||||||
|
this.deeplx = new TranslateTaskRunner(e.default);
|
||||||
|
});
|
||||||
|
import("./services/deeplcustom").then(
|
||||||
|
(e) => (this.deeplcustom = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/freedictionaryapi").then(
|
||||||
|
(e) => (this.freedictionaryapi = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/google").then((e) => {
|
||||||
|
this.googleapi = new TranslateTaskRunner(e.googleapi);
|
||||||
|
this.google = new TranslateTaskRunner(e.google);
|
||||||
|
});
|
||||||
|
import("./services/haici").then(
|
||||||
|
(e) => (this.haici = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/haicidict").then(
|
||||||
|
(e) => (this.haicidict = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/microsoft").then(
|
||||||
|
(e) => (this.microsoft = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/niutrans").then(
|
||||||
|
(e) => (this.niutranspro = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/openl").then(
|
||||||
|
(e) => (this.openl = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/tencent").then(
|
||||||
|
(e) => (this.tencent = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/webliodict").then(
|
||||||
|
(e) => (this.webliodict = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/xftrans").then(
|
||||||
|
(e) => (this.xftrans = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/gpt").then(
|
||||||
|
(e) => (this.gpt = new TranslateTaskRunner(e.gptTranslate))
|
||||||
|
);
|
||||||
|
import("./services/youdao").then(
|
||||||
|
(e) => (this.youdao = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/youdaodict").then(
|
||||||
|
(e) => (this.youdaodict = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
import("./services/youdaozhiyun").then(
|
||||||
|
(e) => (this.youdaozhiyun = new TranslateTaskRunner(e.default))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runTranslationTask(
|
||||||
|
task?: TranslateTask,
|
||||||
|
options: {
|
||||||
|
noCheckZoteroItemLanguage?: boolean;
|
||||||
|
noDisplay?: boolean;
|
||||||
|
} = {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
ztoolkit.log("runTranslationTask", options);
|
||||||
|
task = task || getLastTranslateTask();
|
||||||
|
if (!task || !task.raw) {
|
||||||
|
ztoolkit.log("skipped empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
task.status = "processing" as TranslateTask["status"];
|
||||||
|
// Check whether item language is in disabled languages list
|
||||||
|
let disabledByItemLanguage = false;
|
||||||
|
if (!options.noCheckZoteroItemLanguage && task.itemId) {
|
||||||
|
const item = Zotero.Items.getTopLevel([Zotero.Items.get(task.itemId)])[0];
|
||||||
|
if (item) {
|
||||||
|
const itemLanguage = (
|
||||||
|
(item.getField("language") as string) || ""
|
||||||
|
).split("-")[0];
|
||||||
|
const disabledLanguages = (
|
||||||
|
getPref("disabledLanguages") as string
|
||||||
|
).split(",");
|
||||||
|
disabledByItemLanguage =
|
||||||
|
disabledLanguages.length > 0 &&
|
||||||
|
disabledLanguages.includes(itemLanguage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (disabledByItemLanguage) {
|
||||||
|
ztoolkit.log("disabledByItemLanguage");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Remove possible translation results (for annotations).
|
||||||
|
const splitChar = task.raw.includes(getPref("splitChar") as string)
|
||||||
|
? ""
|
||||||
|
: getPref("splitChar");
|
||||||
|
// /🔤[^🔤]*🔤/g
|
||||||
|
const regex =
|
||||||
|
splitChar === ""
|
||||||
|
? ""
|
||||||
|
: new RegExp(`${splitChar}[^${splitChar}]*${splitChar}`, "g");
|
||||||
|
task.raw = task.raw.replace(regex, "");
|
||||||
|
task.result = "";
|
||||||
|
// Display raw
|
||||||
|
if (!options.noDisplay) {
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
// Get task runner
|
||||||
|
const runner = this[task.service] as TranslateTaskRunner;
|
||||||
|
if (!runner) {
|
||||||
|
task.result = `${task.service} is not implemented.`;
|
||||||
|
task.status = "fail";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Run task
|
||||||
|
await runner.run(task);
|
||||||
|
// Run extra tasks. Do not wait.
|
||||||
|
if (task.extraTasks?.length) {
|
||||||
|
Promise.all(
|
||||||
|
task.extraTasks.map((extraTask) => {
|
||||||
|
return this.runTranslationTask(extraTask, {
|
||||||
|
noCheckZoteroItemLanguage: options.noCheckZoteroItemLanguage,
|
||||||
|
noDisplay: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
).then(() => {
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Try candidate services if current run fails
|
||||||
|
if (task.status === "fail" && task.candidateServices.length > 0) {
|
||||||
|
task.service = task.candidateServices.shift()!;
|
||||||
|
task.status = "waiting";
|
||||||
|
return await this.runTranslationTask(task, options);
|
||||||
|
} else {
|
||||||
|
// Display result
|
||||||
|
if (!options.noDisplay) {
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const success = task.status === "success";
|
||||||
|
const item = Zotero.Items.get(task.itemId!);
|
||||||
|
// Data storage for corresponding types
|
||||||
|
if (success) {
|
||||||
|
switch (task.type) {
|
||||||
|
case "annotation":
|
||||||
|
{
|
||||||
|
if (item) {
|
||||||
|
const savePosition = getPref("annotationTranslationPosition") as
|
||||||
|
| "comment"
|
||||||
|
| "body";
|
||||||
|
const currentText = (
|
||||||
|
(savePosition === "comment"
|
||||||
|
? item.annotationComment
|
||||||
|
: item.annotationText) || ""
|
||||||
|
).replace(regex, "");
|
||||||
|
let text = `${
|
||||||
|
currentText[currentText.length - 1] === "\n" ? "" : "\n"
|
||||||
|
}${splitChar}${task.result}${splitChar}\n`;
|
||||||
|
text = splitChar === "" ? text : `${currentText}${text}`;
|
||||||
|
item[
|
||||||
|
savePosition === "comment"
|
||||||
|
? "annotationComment"
|
||||||
|
: "annotationText"
|
||||||
|
] = text;
|
||||||
|
item.saveTx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "title":
|
||||||
|
{
|
||||||
|
if (item) {
|
||||||
|
ztoolkit.ExtraField.setExtraField(
|
||||||
|
item,
|
||||||
|
"titleTranslation",
|
||||||
|
task.result
|
||||||
|
);
|
||||||
|
item.saveTx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "abstract":
|
||||||
|
{
|
||||||
|
if (item) {
|
||||||
|
ztoolkit.ExtraField.setExtraField(
|
||||||
|
item,
|
||||||
|
"abstractTranslation",
|
||||||
|
// A dirty workaround to make it collapsible on Zotero 6
|
||||||
|
ztoolkit.isZotero7() ? task.result : " " + task.result
|
||||||
|
);
|
||||||
|
item.saveTx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
42
src/modules/services/baidu.ts
Normal file
42
src/modules/services/baidu.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const params = data.secret.split("#");
|
||||||
|
let appid = params[0];
|
||||||
|
let key = params[1];
|
||||||
|
let action = "0";
|
||||||
|
if (params.length >= 3) {
|
||||||
|
action = params[2];
|
||||||
|
}
|
||||||
|
let salt = new Date().getTime();
|
||||||
|
let sign = Zotero.Utilities.Internal.md5(
|
||||||
|
appid + data.raw + salt + key,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
`from=${data.langfrom.split("-")[0]}&to=${data.langto.split("-")[0]}`;
|
||||||
|
|
||||||
|
// Request
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`http://api.fanyi.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(
|
||||||
|
data.raw
|
||||||
|
)}&appid=${appid}&from=${data.langfrom.split("-")[0]}&to=${
|
||||||
|
data.langto.split("-")[0]
|
||||||
|
}&salt=${salt}&sign=${sign}&action=${action}`,
|
||||||
|
{
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
// Parse
|
||||||
|
if (xhr.response.error_code) {
|
||||||
|
throw `Service error: ${xhr.response.error_code}:${xhr.response.error_msg}`;
|
||||||
|
}
|
||||||
|
let tgt = "";
|
||||||
|
for (let i = 0; i < xhr.response.trans_result.length; i++) {
|
||||||
|
tgt += xhr.response.trans_result[i].dst;
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
37
src/modules/services/baidufield.ts
Normal file
37
src/modules/services/baidufield.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const params = data.secret.split("#");
|
||||||
|
let appid = params[0];
|
||||||
|
let key = params[1];
|
||||||
|
let domain = params[2];
|
||||||
|
let salt = new Date().getTime();
|
||||||
|
let sign = Zotero.Utilities.Internal.md5(
|
||||||
|
appid + data.raw + salt + domain + key,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
`from=${data.langfrom.split("-")[0]}&to=${data.langto.split("-")[0]}`;
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`http://api.fanyi.baidu.com/api/trans/vip/fieldtranslate?q=${encodeURIComponent(
|
||||||
|
data.raw
|
||||||
|
)}&appid=${appid}&from=${data.langfrom.split("-")[0]}&to=${
|
||||||
|
data.langto.split("-")[0]
|
||||||
|
}&domain=${domain}&salt=${salt}&sign=${sign}`,
|
||||||
|
{
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
// Parse
|
||||||
|
if (xhr.response.error_code) {
|
||||||
|
throw `Service error: ${xhr.response.error_code}:${xhr.response.error_msg}`;
|
||||||
|
}
|
||||||
|
let tgt = "";
|
||||||
|
for (let i = 0; i < xhr.response.trans_result.length; i++) {
|
||||||
|
tgt += xhr.response.trans_result[i].dst;
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
37
src/modules/services/bingdict.ts
Normal file
37
src/modules/services/bingdict.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`https://cn.bing.com/dict/search?q=${encodeURIComponent(data.raw)}/`,
|
||||||
|
{ responseType: "text" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response;
|
||||||
|
const doc = ztoolkit.getDOMParser().parseFromString(res, "text/html");
|
||||||
|
const mp3s = Array.from(doc.querySelectorAll(".hd_area .bigaud"));
|
||||||
|
const phoneticText = doc.querySelectorAll(".hd_area .b_primtxt");
|
||||||
|
data.audio = mp3s.map((a: Element, i: number) => ({
|
||||||
|
text: phoneticText[i].innerHTML.replace(" ", " "),
|
||||||
|
url: (a.getAttribute("onclick")?.match(/https?:\/\/\S+\.mp3/g) || [""])[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = res.match(/<meta name=\"description\" content=\"(.+) \" ?\/>/gm)[0];
|
||||||
|
} catch (e) {
|
||||||
|
throw "Parse error";
|
||||||
|
}
|
||||||
|
let tgt = "";
|
||||||
|
for (let line of res.split(",").slice(3)) {
|
||||||
|
if (line.indexOf("网络释义") > -1) {
|
||||||
|
tgt += line.slice(0, line.lastIndexOf(";"));
|
||||||
|
} else {
|
||||||
|
tgt += line + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tgt = tgt.replace(/" \/>/g, "");
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
26
src/modules/services/caiyun.ts
Normal file
26
src/modules/services/caiyun.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
let param = `${data.langfrom.split("-")[0]}2${data.langto.split("-")[0]}`;
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"http://api.interpreter.caiyunai.com/v1/translator",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-authorization": `token ${data.secret}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
source: [data.raw],
|
||||||
|
trans_type: param,
|
||||||
|
request_id: new Date().valueOf() / 10000,
|
||||||
|
detect: true,
|
||||||
|
}),
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.target[0];
|
||||||
|
};
|
108
src/modules/services/cnki.ts
Normal file
108
src/modules/services/cnki.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { aesEcbEncrypt, base64 } from "../../utils/crypto";
|
||||||
|
import { getPref, setPref } from "../../utils/prefs";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
if (data.raw.length > 1000) {
|
||||||
|
new ztoolkit.ProgressWindow("PDF Translate")
|
||||||
|
.createLine({
|
||||||
|
text: `Maximam text length is 1000, ${data.raw.length} selected. Will only translate first 1000 characters.`,
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
data.raw = data.raw.slice(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://dict.cnki.net/fyzs-front-api/translate/literaltranslation",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
Token: await getToken(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
words: await getWord(data.raw),
|
||||||
|
translateType: null,
|
||||||
|
}),
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xhr.response.data?.isInputVerificationCode) {
|
||||||
|
throw "Your access is temporarily banned by the CNKI service. Please goto https://dict.cnki.net/, translate manually and pass human verification.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (retry && xhr.response.data?.isInputVerificationCode) {
|
||||||
|
// // Monitor verification
|
||||||
|
// await Zotero.HTTP.request(
|
||||||
|
// "GET",
|
||||||
|
// "https://dict.cnki.net/fyzs-front-api/captchaImage",
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// "Content-Type": "application/json;charset=UTF-8",
|
||||||
|
// Token: await getToken(),
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// await Zotero.HTTP.request(
|
||||||
|
// "POST",
|
||||||
|
// "https://dict.cnki.net/fyzs-front-api/translate/addVerificationCodeTimes",
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// "Content-Type": "application/json;charset=UTF-8",
|
||||||
|
// Token: await getToken(),
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// await getToken(true);
|
||||||
|
// // Call translation again
|
||||||
|
// return await cnki.call(this, text, false);
|
||||||
|
// // throw "CNKI requires verification. Please verify manually in popup or open dict.cnki.net in browser.";
|
||||||
|
// }
|
||||||
|
let tgt = xhr.response.data?.mResult;
|
||||||
|
tgt = tgt.replace('(查看名企职位领高薪offer!--->智联招聘https://dict.cnki.net/ad.html)', '');
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getToken(forceRefresh: boolean = false) {
|
||||||
|
let token = "";
|
||||||
|
// Just in case the update fails
|
||||||
|
let doRefresh = true;
|
||||||
|
try {
|
||||||
|
const tokenObj = JSON.parse(getPref("cnkiToken") as string);
|
||||||
|
if (
|
||||||
|
!forceRefresh &&
|
||||||
|
tokenObj?.token &&
|
||||||
|
new Date().getTime() - tokenObj.t < 300 * 1000
|
||||||
|
) {
|
||||||
|
token = tokenObj.token;
|
||||||
|
doRefresh = false;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (doRefresh) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
"https://dict.cnki.net/fyzs-front-api/getToken",
|
||||||
|
{
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr && xhr.response && xhr.response.code === 200) {
|
||||||
|
token = xhr.response.token;
|
||||||
|
setPref(
|
||||||
|
"cnkiToken",
|
||||||
|
JSON.stringify({
|
||||||
|
t: new Date().getTime(),
|
||||||
|
token: xhr.response.data,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWord(text: string) {
|
||||||
|
const encrtypted = await aesEcbEncrypt(text, "4e87183cfd3a45fe");
|
||||||
|
const base64str = base64(encrtypted);
|
||||||
|
return base64str.replace(/\//g, "_").replace(/\+/g, "-");
|
||||||
|
}
|
41
src/modules/services/collinsdict.ts
Normal file
41
src/modules/services/collinsdict.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
"https://www.collinsdictionary.com/zh/dictionary/english-chinese/" +
|
||||||
|
encodeURIComponent(data.raw),
|
||||||
|
{ responseType: "text" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.responseURL.includes("?q=")) {
|
||||||
|
throw "No result found error";
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = ztoolkit
|
||||||
|
.getDOMParser()
|
||||||
|
.parseFromString(xhr.response, "text/html");
|
||||||
|
Array.prototype.forEach.call(doc.querySelectorAll("script"), (e) =>
|
||||||
|
e.remove()
|
||||||
|
);
|
||||||
|
|
||||||
|
const phoneticElements = Array.from(
|
||||||
|
doc.querySelectorAll(".type-")
|
||||||
|
) as HTMLElement[];
|
||||||
|
data.audio = phoneticElements.map((e) => ({
|
||||||
|
text: e.innerText.trim(),
|
||||||
|
url: e.querySelector("a")?.getAttribute("data-src-mp3") || "",
|
||||||
|
}));
|
||||||
|
// script in innerText
|
||||||
|
const explanationText: string = Array.prototype.map
|
||||||
|
.call(doc.querySelectorAll(".hom"), (e: HTMLSpanElement) =>
|
||||||
|
e.innerText.replace(/ /g, " ").replace(/[0-9]\./g, "\n$&")
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
data.result = explanationText;
|
||||||
|
};
|
25
src/modules/services/deepl.ts
Normal file
25
src/modules/services/deepl.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { TranslateTask, TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export const deeplfree = <TranslateTaskProcessor>async function (data) {
|
||||||
|
return await deepl("https://api-free.deepl.com/v2/translate", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deeplpro = <TranslateTaskProcessor>async function (data) {
|
||||||
|
return await deepl("https://api.deepl.com/v2/translate", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function deepl(url: string, data: Required<TranslateTask>) {
|
||||||
|
const reqBody = `auth_key=${data.secret}&text=${encodeURIComponent(
|
||||||
|
data.raw
|
||||||
|
)}&source_lang=${data.langfrom
|
||||||
|
.split("-")[0]
|
||||||
|
.toUpperCase()}&target_lang=${data.langto.split("-")[0].toUpperCase()}`;
|
||||||
|
const xhr = await Zotero.HTTP.request("POST", url, {
|
||||||
|
responseType: "json",
|
||||||
|
body: reqBody,
|
||||||
|
});
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.translations[0].text;
|
||||||
|
}
|
23
src/modules/services/deeplcustom.ts
Normal file
23
src/modules/services/deeplcustom.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const url = data.secret;
|
||||||
|
const reqBody = JSON.stringify(
|
||||||
|
{
|
||||||
|
text: data.raw,
|
||||||
|
source_lang: data.langfrom.split("-")[0].toUpperCase(),
|
||||||
|
target_lang: data.langto.split("-")[0].toUpperCase()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const xhr = await Zotero.HTTP.request("POST", url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
body: reqBody,
|
||||||
|
});
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.data;
|
||||||
|
}
|
70
src/modules/services/deeplx.ts
Normal file
70
src/modules/services/deeplx.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const id = 1000*(Math.floor(Math.random() * 99999) + 8300000)+1;
|
||||||
|
const url = "https://www2.deepl.com/jsonrpc";
|
||||||
|
const t = data.raw;
|
||||||
|
var ICounts = 0;
|
||||||
|
var ts = Date.now();
|
||||||
|
for (var i = 0; i < t.length; i++) {
|
||||||
|
if (t[i] == "i") {
|
||||||
|
ICounts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ICounts != 0) {
|
||||||
|
ICounts++;
|
||||||
|
ts = ts - ts%ICounts + ICounts
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var reqBody = JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "LMT_handle_texts",
|
||||||
|
id: id,
|
||||||
|
params: {
|
||||||
|
texts: [
|
||||||
|
{
|
||||||
|
text: t,
|
||||||
|
requestAlternatives: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
splitting: "newlines",
|
||||||
|
lang: {
|
||||||
|
source_lang_user_selected: data.langfrom
|
||||||
|
.split("-")[0]
|
||||||
|
.toUpperCase(),
|
||||||
|
target_lang: data.langto.split("-")[0].toUpperCase(),
|
||||||
|
},
|
||||||
|
timestamp: ts,
|
||||||
|
commonJobParams: {
|
||||||
|
wasSpoken: false,
|
||||||
|
transcribe_as: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if ((id+5)%29 == 0 || (id+3)%13 == 0) {
|
||||||
|
reqBody = reqBody.replace('"method":"', '"method" : "');
|
||||||
|
} else {
|
||||||
|
reqBody = reqBody.replace('"method":"', '"method": "');
|
||||||
|
}
|
||||||
|
const xhr = await Zotero.HTTP.request("POST", url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"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.6.0 iOS 16.3.0 (iPhone13,2)",
|
||||||
|
"x-app-build": "353933",
|
||||||
|
"x-app-version": "2.6",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
body: reqBody,
|
||||||
|
});
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.result.texts[0].text;
|
||||||
|
};
|
40
src/modules/services/freedictionaryapi.ts
Normal file
40
src/modules/services/freedictionaryapi.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`https://api.dictionaryapi.dev/api/v2/entries/en/${data.raw}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = xhr.response[0];
|
||||||
|
let tgt = "";
|
||||||
|
if (res.phonetics) {
|
||||||
|
tgt += res.phonetics.map((p: any) => p.text).join(",");
|
||||||
|
tgt += "\n";
|
||||||
|
}
|
||||||
|
if (res.meanings) {
|
||||||
|
tgt += res.meanings
|
||||||
|
.map(
|
||||||
|
(m: any) =>
|
||||||
|
`[${m.partOfSpeech}] ${m.definitions
|
||||||
|
.map(
|
||||||
|
(d: any) =>
|
||||||
|
`${d.definition}\n${
|
||||||
|
d.example ? `\t[example] ${d.example}` : ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
.join("")}`
|
||||||
|
)
|
||||||
|
.join("----\n");
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
85
src/modules/services/google.ts
Normal file
85
src/modules/services/google.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { TranslateTask, TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export const googleapi = <TranslateTaskProcessor>async function (data) {
|
||||||
|
return await _google("https://translate.googleapis.com", data);
|
||||||
|
};
|
||||||
|
export const google = <TranslateTaskProcessor>async function (data) {
|
||||||
|
return await _google("https://translate.google.com", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function _google(url: string, data: Required<TranslateTask>) {
|
||||||
|
function TL(a: any) {
|
||||||
|
var k = "";
|
||||||
|
var b = 406644;
|
||||||
|
var b1 = 3293161072;
|
||||||
|
|
||||||
|
var jd = ".";
|
||||||
|
var $b = "+-a^+6";
|
||||||
|
var Zb = "+-3^+b+-f";
|
||||||
|
|
||||||
|
for (var e = [], f = 0, g = 0; g < a.length; g++) {
|
||||||
|
var m = a.charCodeAt(g);
|
||||||
|
128 > m
|
||||||
|
? (e[f++] = m)
|
||||||
|
: (2048 > m
|
||||||
|
? (e[f++] = (m >> 6) | 192)
|
||||||
|
: (55296 == (m & 64512) &&
|
||||||
|
g + 1 < a.length &&
|
||||||
|
56320 == (a.charCodeAt(g + 1) & 64512)
|
||||||
|
? ((m =
|
||||||
|
65536 + ((m & 1023) << 10) + (a.charCodeAt(++g) & 1023)),
|
||||||
|
(e[f++] = (m >> 18) | 240),
|
||||||
|
(e[f++] = ((m >> 12) & 63) | 128))
|
||||||
|
: (e[f++] = (m >> 12) | 224),
|
||||||
|
(e[f++] = ((m >> 6) & 63) | 128)),
|
||||||
|
(e[f++] = (m & 63) | 128));
|
||||||
|
}
|
||||||
|
a = b;
|
||||||
|
for (f = 0; f < e.length; f++) (a += e[f]), (a = RL(a, $b));
|
||||||
|
a = RL(a, Zb);
|
||||||
|
a ^= b1 || 0;
|
||||||
|
0 > a && (a = (a & 2147483647) + 2147483648);
|
||||||
|
a %= 1e6;
|
||||||
|
return a.toString() + jd + (a ^ b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RL(a: any, b: any) {
|
||||||
|
var t = "a";
|
||||||
|
var Yb = "+";
|
||||||
|
for (var c = 0; c < b.length - 2; c += 3) {
|
||||||
|
var d = b.charAt(c + 2),
|
||||||
|
// @ts-ignore
|
||||||
|
d = d >= t ? d.charCodeAt(0) - 87 : Number(d),
|
||||||
|
// @ts-ignore
|
||||||
|
d = b.charAt(c + 1) == Yb ? a >>> d : a << d;
|
||||||
|
a = b.charAt(c) == Yb ? (a + d) & 4294967295 : a ^ d;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
let param = `sl=${data.langfrom}&tl=${data.langto}`;
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`${
|
||||||
|
data.secret ? data.secret : url
|
||||||
|
}/translate_a/single?client=gtx&${param}&hl=zh-CN&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&source=bh&ssel=0&tsel=0&kc=1&tk=${TL(
|
||||||
|
data.raw
|
||||||
|
)}&q=${encodeURIComponent(data.raw)}`,
|
||||||
|
{ responseType: "json" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tgt = "";
|
||||||
|
for (let i = 0; i < xhr.response[0].length; i++) {
|
||||||
|
if (!xhr.response[0][i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (xhr.response[0][i] && xhr.response[0][i][0]) {
|
||||||
|
tgt += xhr.response[0][i][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
}
|
97
src/modules/services/gpt.ts
Normal file
97
src/modules/services/gpt.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
import { getPref } from "../../utils/prefs";
|
||||||
|
import { getServiceSecret } from "../../utils/translate";
|
||||||
|
|
||||||
|
export const gptTranslate = <TranslateTaskProcessor>async function (data) {
|
||||||
|
const model = getPref("gptModel");
|
||||||
|
const temperature = parseFloat(getPref("gptTemperature") as string);
|
||||||
|
const apiUrl = getPref("gptUrl");
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
apiUrl,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${data.secret}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `As an academic expert with specialized knowledge in various fields, please provide a proficient and precise translation translation from ${data.langfrom.split("-")[0]} to ${data.langto.split("-")[0]} of the academic text enclosed in 🔤. It is crucial to maintaining the original phrase or sentence and ensure accuracy while utilizing the appropriate language. The text is as follows: 🔤 ${data.raw} 🔤 Please provide the translated result without any additional explanation and remove 🔤.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: temperature,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
responseType: "text",
|
||||||
|
requestObserver: (xmlhttp: XMLHttpRequest) => {
|
||||||
|
let preLength = 0;
|
||||||
|
let result = "";
|
||||||
|
xmlhttp.onprogress = (e: any) => {
|
||||||
|
// Only concatenate the new strings
|
||||||
|
let newResponse = e.target.response.slice(preLength);
|
||||||
|
let dataArray = newResponse.split("data: ");
|
||||||
|
|
||||||
|
for (let data of dataArray) {
|
||||||
|
try {
|
||||||
|
let obj = JSON.parse(data);
|
||||||
|
let choice = obj.choices[0];
|
||||||
|
if (choice.finish_reason) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result += choice.delta.content || "";
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear timeouts caused by stream transfers
|
||||||
|
if (e.target.timeout) {
|
||||||
|
e.target.timeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove \n\n from the beginning of the data
|
||||||
|
data.result = result.replace(/^\n\n/, "");
|
||||||
|
preLength = e.target.response.length;
|
||||||
|
|
||||||
|
if (data.type === "text") {
|
||||||
|
addon.hooks.onReaderPopupRefresh();
|
||||||
|
addon.hooks.onReaderTabPanelRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
// data.result = xhr.response.choices[0].message.content.substr(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateGPTModel = async function () {
|
||||||
|
const secret = getServiceSecret("gpt");
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
"https://api.openai.com/v1/models",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${secret}`,
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const models = xhr.response.data;
|
||||||
|
const availableModels = [];
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
if (model.id.includes("gpt")) {
|
||||||
|
availableModels.push(model.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableModels;
|
||||||
|
};
|
66
src/modules/services/haici.ts
Normal file
66
src/modules/services/haici.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { getPref, setPref } from "../../utils/prefs";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`http://api.microsofttranslator.com/V2/Ajax.svc/TranslateArray?appId=${await getAppId()}&from=${
|
||||||
|
data.langfrom
|
||||||
|
}&to=${data.langto}&texts=["${encodeURIComponent(data.raw)}"]`,
|
||||||
|
{ responseType: "json" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let tgt = "";
|
||||||
|
xhr.response.forEach((line: { TranslatedText: string }) => {
|
||||||
|
tgt += line.TranslatedText;
|
||||||
|
});
|
||||||
|
data.result = tgt;
|
||||||
|
} catch {
|
||||||
|
throw `Service error: ${xhr.response}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getAppId(forceRefresh: boolean = false) {
|
||||||
|
let appId = "";
|
||||||
|
// Just in case the update fails
|
||||||
|
let doRefresh = true;
|
||||||
|
try {
|
||||||
|
const appIdObj = JSON.parse(getPref("haiciAppId") as string);
|
||||||
|
if (
|
||||||
|
!forceRefresh &&
|
||||||
|
appIdObj &&
|
||||||
|
appIdObj.appId &&
|
||||||
|
new Date().getTime() - appIdObj.t < 60 * 60 * 1000
|
||||||
|
) {
|
||||||
|
appId = appIdObj.appId;
|
||||||
|
doRefresh = false;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (doRefresh) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
"http://capi.dict.cn/fanyi.php",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Referer: "http://fanyi.dict.cn/",
|
||||||
|
},
|
||||||
|
responseType: "text",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr && xhr.response) {
|
||||||
|
appId = xhr.response.match(/"(.+)"/)[1];
|
||||||
|
setPref(
|
||||||
|
"haiciAppId",
|
||||||
|
JSON.stringify({
|
||||||
|
t: new Date().getTime(),
|
||||||
|
appId: appId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appId;
|
||||||
|
}
|
39
src/modules/services/haicidict.ts
Normal file
39
src/modules/services/haicidict.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request("GET", `http://dict.cn/${data.raw}`, {
|
||||||
|
responseType: "text",
|
||||||
|
});
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response as string;
|
||||||
|
let tgt = "";
|
||||||
|
try {
|
||||||
|
const audioRegex = /naudio="(\w+.mp3\?t=\w+?)"/;
|
||||||
|
data.audio =
|
||||||
|
res.match(new RegExp(audioRegex, "gi"))?.map((s: string) => ({
|
||||||
|
text: "",
|
||||||
|
url: "http://audio.dict.cn/" + s.match(new RegExp(audioRegex, "i"))![1],
|
||||||
|
})) || [];
|
||||||
|
const symbolsRegex = /<span>(.)[\n\t\s]*?<bdo lang="EN-US">(.+?)<\/bdo>/;
|
||||||
|
let symbols: string[] = [];
|
||||||
|
res.match(new RegExp(symbolsRegex, "g"))!.forEach((line) => {
|
||||||
|
let [_, country, sym] = line.match(symbolsRegex)!;
|
||||||
|
symbols.push(`${country} ${sym}`);
|
||||||
|
});
|
||||||
|
tgt += symbols.join("\n") + "\n";
|
||||||
|
res = res.match(/<ul class="dict-basic-ul">[\s\S]+?<\/ul>/)![0];
|
||||||
|
} catch (e) {
|
||||||
|
throw "Parse error";
|
||||||
|
}
|
||||||
|
for (let line of res.match(/<li>[\s\S]+?<\/li>/g) || []) {
|
||||||
|
tgt +=
|
||||||
|
line
|
||||||
|
.replace(/<\/?.+?>/g, "")
|
||||||
|
.replace(/[\n\t]+/g, " ")
|
||||||
|
.trim() + "\n";
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
33
src/modules/services/microsoft.ts
Normal file
33
src/modules/services/microsoft.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const req_body = JSON.stringify([
|
||||||
|
{
|
||||||
|
text: data.raw,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
`https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${data.langto}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
Host: "api.cognitive.microsofttranslator.com",
|
||||||
|
"Content-Length": req_body.length,
|
||||||
|
"Ocp-Apim-Subscription-Key": data.secret,
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
body: req_body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = xhr.response[0].translations[0].text;
|
||||||
|
if (!result) {
|
||||||
|
throw `Parse error: ${JSON.stringify(xhr.response)}`;
|
||||||
|
}
|
||||||
|
data.result = result;
|
||||||
|
};
|
37
src/modules/services/niutrans.ts
Normal file
37
src/modules/services/niutrans.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { getPref } from "../../utils/prefs";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const apikey = data.secret;
|
||||||
|
const dictNo = getPref("niutransDictNo");
|
||||||
|
const memoryNo = getPref("niutransMemoryNo");
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://api.niutrans.com/NiuTransServer/translation",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
from: data.langfrom.split("-")[0],
|
||||||
|
to: data.langto.split("-")[0],
|
||||||
|
apikey,
|
||||||
|
dictNo,
|
||||||
|
memoryNo,
|
||||||
|
source: "zotero",
|
||||||
|
src_text: data.raw,
|
||||||
|
}),
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.response.error_code) {
|
||||||
|
throw `Service error: ${xhr.response.error_code}:${xhr.response.error_msg}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.tgt_text;
|
||||||
|
};
|
54
src/modules/services/openl.ts
Normal file
54
src/modules/services/openl.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
let [services, apikey] = data.secret.split("#");
|
||||||
|
const serviceList = services.split(",");
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://api.openl.club/group/translate",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
apikey: apikey,
|
||||||
|
services: serviceList,
|
||||||
|
text: data.raw,
|
||||||
|
source_lang: data.langfrom.split("-")[0],
|
||||||
|
target_lang: data.langto.split("-")[0],
|
||||||
|
}),
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response as {
|
||||||
|
status: boolean | any;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
if (!res.status) {
|
||||||
|
throw `Service error: ${JSON.stringify(res)}`;
|
||||||
|
}
|
||||||
|
delete res.status;
|
||||||
|
let tgt = "";
|
||||||
|
const openLServices = Object.keys(res);
|
||||||
|
if (openLServices.length === 1) {
|
||||||
|
// Only one engine
|
||||||
|
const resObj = res[openLServices[0]];
|
||||||
|
if (resObj.status) {
|
||||||
|
tgt = resObj.result;
|
||||||
|
} else {
|
||||||
|
throw "Service error: all OpenL services failed.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let openLService of openLServices) {
|
||||||
|
if (res[openLService].status) {
|
||||||
|
tgt += `[${openLService}] ${res[openLService].result}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
66
src/modules/services/tencent.ts
Normal file
66
src/modules/services/tencent.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { base64, hmacSha1Digest } from "../../utils/crypto";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
let params = data.secret.split("#");
|
||||||
|
let secretId = params[0];
|
||||||
|
let secretKey = params[1];
|
||||||
|
let region = "ap-shanghai";
|
||||||
|
if (params.length >= 3) {
|
||||||
|
region = params[2];
|
||||||
|
}
|
||||||
|
let projectId = "0";
|
||||||
|
if (params.length >= 4) {
|
||||||
|
projectId = params[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeRFC5987ValueChars(str: string) {
|
||||||
|
return encodeURIComponent(str)
|
||||||
|
.replace(
|
||||||
|
/['()]/g,
|
||||||
|
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||||
|
) // i.e., %27 %28 %29
|
||||||
|
.replace(/\*/g, "%2A")
|
||||||
|
.replace(/%20/g, "+");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawStr = `Action=TextTranslate&Language=zh-CN&Nonce=9744&ProjectId=${projectId}&Region=${region}&SecretId=${secretId}&Source=${
|
||||||
|
data.langfrom.split("-")[0]
|
||||||
|
}&SourceText=#$#&Target=${data.langto.split("-")[0]}&Timestamp=${new Date()
|
||||||
|
.getTime()
|
||||||
|
.toString()
|
||||||
|
.substring(0, 10)}&Version=2018-03-21`;
|
||||||
|
|
||||||
|
const sha1Str = encodeRFC5987ValueChars(
|
||||||
|
base64(
|
||||||
|
await hmacSha1Digest(
|
||||||
|
`POSTtmt.tencentcloudapi.com/?${rawStr.replace("#$#", data.raw)}`,
|
||||||
|
secretKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://tmt.tencentcloudapi.com",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
// Encode \s to +
|
||||||
|
body: `${rawStr.replace(
|
||||||
|
"#$#",
|
||||||
|
encodeRFC5987ValueChars(data.raw)
|
||||||
|
)}&Signature=${sha1Str}`,
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.response.Response.Error) {
|
||||||
|
throw `Service error: ${xhr.response.Response.Error.Code}:${xhr.response.Response.Error.Message}`;
|
||||||
|
}
|
||||||
|
data.result = xhr.response.Response.TargetText;
|
||||||
|
};
|
45
src/modules/services/webliodict.ts
Normal file
45
src/modules/services/webliodict.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`https://ejje.weblio.jp/content/${encodeURIComponent(data.raw)}/`,
|
||||||
|
{ responseType: "text" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response;
|
||||||
|
const doc: Document = ztoolkit
|
||||||
|
.getDOMParser()
|
||||||
|
.parseFromString(res, "text/html");
|
||||||
|
const translations: string[][] = [];
|
||||||
|
|
||||||
|
const process = (ele: Element | undefined) => {
|
||||||
|
if (!ele) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(ele.children).map((e) =>
|
||||||
|
(e as HTMLElement).innerText.trim()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
translations.push(process(doc.querySelector(".descriptionWrp")?.children[0]));
|
||||||
|
doc.querySelector(".descriptionWrp")?.remove();
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
doc.querySelector(".summaryM")?.children,
|
||||||
|
(e: Element) => translations.push(process(e))
|
||||||
|
);
|
||||||
|
|
||||||
|
Array.from(doc.querySelectorAll(".intrst"))
|
||||||
|
.map((e: Element) => e.querySelector("tr"))
|
||||||
|
.forEach((e) => {
|
||||||
|
e && translations.push(process(e));
|
||||||
|
});
|
||||||
|
data.result = translations
|
||||||
|
.filter((t) => t)
|
||||||
|
.map((t) => t.join(":"))
|
||||||
|
.join("\n");
|
||||||
|
};
|
87
src/modules/services/xftrans.ts
Normal file
87
src/modules/services/xftrans.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { base64, hmacSha256Digest, sha256Digest } from "../../utils/crypto";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
let [appid, apiSecret, apiKey] = data.secret.split("#");
|
||||||
|
const config = {
|
||||||
|
appid,
|
||||||
|
apiSecret,
|
||||||
|
apiKey,
|
||||||
|
host: "itrans.xfyun.cn",
|
||||||
|
hostUrl: "https://itrans.xfyun.cn/v2/its",
|
||||||
|
uri: "/v2/its",
|
||||||
|
};
|
||||||
|
|
||||||
|
function transLang(inlang: string = "") {
|
||||||
|
const langs = [{ regex: /zh-\w+/, lang: "cn" }];
|
||||||
|
// default
|
||||||
|
let outlang = inlang.split("-")[0];
|
||||||
|
langs.forEach((obj) => {
|
||||||
|
if (obj.regex.test(inlang)) {
|
||||||
|
outlang = obj.lang;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return outlang;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transVar = {
|
||||||
|
text: data.raw,
|
||||||
|
from: transLang(data.langfrom),
|
||||||
|
to: transLang(data.langto),
|
||||||
|
};
|
||||||
|
const date = new Date().toUTCString();
|
||||||
|
const postBody = getPostBody(transVar.text, transVar.from, transVar.to);
|
||||||
|
const digest = await getDigest(postBody);
|
||||||
|
const options = {
|
||||||
|
url: config.hostUrl,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json,version=1.0",
|
||||||
|
Host: config.host,
|
||||||
|
Date: date,
|
||||||
|
Digest: digest,
|
||||||
|
Authorization: await getAuthStr(date, digest),
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
body: postBody,
|
||||||
|
};
|
||||||
|
function getPostBody(text: string, from: string, to: string) {
|
||||||
|
const digestObj = {
|
||||||
|
common: {
|
||||||
|
app_id: config.appid,
|
||||||
|
},
|
||||||
|
business: {
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
text: base64(new TextEncoder().encode(text)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return digestObj;
|
||||||
|
}
|
||||||
|
async function getDigest(body: any) {
|
||||||
|
return `SHA-256=${base64(await sha256Digest(JSON.stringify(body)))}`;
|
||||||
|
}
|
||||||
|
async function getAuthStr(date: string, digest: string) {
|
||||||
|
const signatureOrigin = `host: ${config.host}\ndate: ${date}\nPOST ${config.uri} HTTP/1.1\ndigest: ${digest}`;
|
||||||
|
const signatureSha = await hmacSha256Digest(
|
||||||
|
signatureOrigin,
|
||||||
|
config.apiSecret
|
||||||
|
);
|
||||||
|
const signature = base64(signatureSha);
|
||||||
|
const authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"`;
|
||||||
|
return authorizationOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request("POST", options.url, {
|
||||||
|
headers: options.headers,
|
||||||
|
responseType: "json",
|
||||||
|
body: JSON.stringify(options.body),
|
||||||
|
});
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.result = xhr.response.data.result.trans_result.dst;
|
||||||
|
};
|
27
src/modules/services/youdao.ts
Normal file
27
src/modules/services/youdao.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
let param = `${data.langfrom.toUpperCase().replace("-", "_")}2${data.langto
|
||||||
|
.toUpperCase()
|
||||||
|
.replace("-", "_")}`;
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`http://fanyi.youdao.com/translate?&doctype=json&type=${param}&i=${encodeURIComponent(
|
||||||
|
data.raw
|
||||||
|
)}`,
|
||||||
|
{ responseType: "json" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response.translateResult;
|
||||||
|
let tgt = "";
|
||||||
|
for (let i in res) {
|
||||||
|
for (let j in res[i]) {
|
||||||
|
tgt += res[i][j].tgt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
28
src/modules/services/youdaodict.ts
Normal file
28
src/modules/services/youdaodict.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`https://www.youdao.com/w/${encodeURIComponent(data.raw)}/`,
|
||||||
|
{ responseType: "text" }
|
||||||
|
);
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response;
|
||||||
|
res = res.replace(/(\r\n|\n|\r)/gm, "");
|
||||||
|
res = res.match(
|
||||||
|
/<div id="phrsListTab.*webTrans" class="trans-wrapper trans-tab">/gm
|
||||||
|
);
|
||||||
|
|
||||||
|
let tgt = "";
|
||||||
|
if (res.length > 0) {
|
||||||
|
tgt = res[0].replace(/<[^>]*>?/gm, "\n");
|
||||||
|
tgt = tgt.replace(/\n\s*\n/g, "\n");
|
||||||
|
tgt = tgt.replace(/\s\s+/g, " ");
|
||||||
|
tgt = tgt.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.result = tgt;
|
||||||
|
};
|
53
src/modules/services/youdaozhiyun.ts
Normal file
53
src/modules/services/youdaozhiyun.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { hex, sha256Digest } from "../../utils/crypto";
|
||||||
|
import { TranslateTaskProcessor } from "../../utils/translate";
|
||||||
|
|
||||||
|
export default <TranslateTaskProcessor>async function (data) {
|
||||||
|
function encodeRFC5987ValueChars(str: string) {
|
||||||
|
return encodeURIComponent(str)
|
||||||
|
.replace(
|
||||||
|
/['()]/g,
|
||||||
|
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||||
|
) // i.e., %27 %28 %29
|
||||||
|
.replace(/\*/g, "%2A")
|
||||||
|
.replace(/%20/g, "+");
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(q: string) {
|
||||||
|
const len = q.length;
|
||||||
|
if (len <= 20) return q;
|
||||||
|
return q.substring(0, 10) + len + q.substring(len - 10, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [appid, key, vocabId] = data.secret.split("#");
|
||||||
|
const salt = new Date().getTime();
|
||||||
|
const curtime = Math.round(new Date().getTime() / 1000);
|
||||||
|
const query = data.raw;
|
||||||
|
const from = data.langfrom;
|
||||||
|
const to = data.langto;
|
||||||
|
const str1 = appid + truncate(query) + salt + curtime + key;
|
||||||
|
|
||||||
|
const sign = hex(await sha256Digest(str1));
|
||||||
|
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
`https://openapi.youdao.com/api?q=${encodeRFC5987ValueChars(
|
||||||
|
query
|
||||||
|
)}&appKey=${appid}&salt=${salt}&from=${from}&to=${to}&sign=${sign}&signType=v3&curtime=${curtime}&vocabId=${vocabId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xhr?.status !== 200) {
|
||||||
|
throw `Request error: ${xhr?.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = xhr.response;
|
||||||
|
if (parseInt(res.errorCode) !== 0) {
|
||||||
|
throw `Service error: ${res.errorCode}`;
|
||||||
|
}
|
||||||
|
data.result = res.translation.join("");
|
||||||
|
};
|
20
src/modules/shortcuts.ts
Normal file
20
src/modules/shortcuts.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
|
||||||
|
export function registerShortcuts() {
|
||||||
|
ztoolkit.Shortcut.register("element", {
|
||||||
|
id: `${config.addonRef}-translateKey`,
|
||||||
|
key: "T",
|
||||||
|
modifiers: "accel",
|
||||||
|
xulData: {
|
||||||
|
document,
|
||||||
|
command: `${config.addonRef}-translateCmd`,
|
||||||
|
_parentId: `${config.addonRef}-keyset`,
|
||||||
|
_commandOptions: {
|
||||||
|
id: `${config.addonRef}-translateCmd`,
|
||||||
|
document,
|
||||||
|
_parentId: `${config.addonRef}-cmdset`,
|
||||||
|
oncommand: `Zotero.${config.addonInstance}.hooks.onShortcuts(Zotero_Tabs.selectedType)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
1011
src/modules/tabpanel.ts
Normal file
1011
src/modules/tabpanel.ts
Normal file
File diff suppressed because it is too large
Load Diff
569
src/utils/config.ts
Normal file
569
src/utils/config.ts
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
interface TranslateService {
|
||||||
|
type: "word" | "sentence";
|
||||||
|
id: string;
|
||||||
|
defaultSecret?: string;
|
||||||
|
secretValidator?: (secret: string) => SecretValidateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecretValidateResult {
|
||||||
|
secret: string;
|
||||||
|
status: boolean;
|
||||||
|
info: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SERVICES: Readonly<Readonly<TranslateService>[]> = <const>[
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "googleapi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "google",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "cnki",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "haici",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "youdao",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "youdaozhiyun",
|
||||||
|
defaultSecret: "appid#appsecret#vocabid(optional)",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = [2, 3].includes(parts.length);
|
||||||
|
const partsInfo = `AppID: ${parts[0]}\nAppKey: ${parts[1]}\nVocabID: ${
|
||||||
|
parts[2] ? parts[2] : ""
|
||||||
|
}`;
|
||||||
|
const source = getService("youdaozhiyun");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of YoudaoZhiyun is AppID#AppKey#VocabID(optional). The secret must have 2 or 3 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "niutranspro",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const flag = secret?.length === 32;
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag,
|
||||||
|
info: flag
|
||||||
|
? ""
|
||||||
|
: `The secret is your NiuTrans API-KEY. The secret length must be 32, but got ${secret?.length}.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "microsoft",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const flag = secret?.length === 32;
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag,
|
||||||
|
info: flag
|
||||||
|
? ""
|
||||||
|
: `The secret is your Azure translate service KEY. The secret length must be 32, but got ${secret?.length}.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "caiyun",
|
||||||
|
defaultSecret: "3975l6lr5pcbvidl6jl2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "deeplfree",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const flag = secret?.length >= 36;
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag,
|
||||||
|
info: flag
|
||||||
|
? ""
|
||||||
|
: `The secret is your DeepL (free plan) KEY. The secret length must >= 36, but got ${secret?.length}.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "deeplpro",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const flag = secret?.length >= 36;
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag,
|
||||||
|
info: flag
|
||||||
|
? ""
|
||||||
|
: `The secret is your DeepL (pro plan) KEY. The secret length must >= 36, but got ${secret?.length}.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "deeplcustom",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const flag = Boolean(secret);
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag,
|
||||||
|
info: flag ? "" : `Please enter custom DeepL URL.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "deeplx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "baidu",
|
||||||
|
defaultSecret: "appid#key",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = [2, 3].includes(parts.length);
|
||||||
|
const partsInfo = `AppID: ${parts[0]}\nKey: ${parts[1]}\nAction: ${
|
||||||
|
parts[2] ? parts[2] : "0"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const source = getService("baidu");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of Baidu Text Translation is AppID#Key#Action(optional). The secret must have 2 or 3 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "baidufield",
|
||||||
|
defaultSecret: "appid#key#field",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = parts.length === 3;
|
||||||
|
const partsInfo = `AppID: ${parts[0]}\nKey: ${parts[1]}\nDomainCode: ${parts[2]}`;
|
||||||
|
const source = getService("baidufield");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of Baidu Domain Text Translation is AppID#Key#DomainCode. The secret must have 3 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "openl",
|
||||||
|
defaultSecret: "service1,service2,...#apikey",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = parts.length === 2;
|
||||||
|
const partsInfo = `Services: ${parts[0]}\nAPIKey: ${parts[1]}`;
|
||||||
|
const source = getService("openl");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of OpenL is service1,service2,...#APIKey. The secret must have 2 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "tencent",
|
||||||
|
defaultSecret:
|
||||||
|
"secretId#SecretKey#Region(default ap-shanghai)#ProjectId(default 0)",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = [2, 3, 4].includes(parts.length);
|
||||||
|
const partsInfo = `SecretId: ${parts[0]}\nSecretKey: ${
|
||||||
|
parts[1]
|
||||||
|
}\nRegion: ${parts[2] ? parts[2] : "ap-shanghai"}\nProjectId: ${
|
||||||
|
parts[3] ? parts[3] : "0"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const source = getService("tencent");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of Tencent Translation is SecretId#SecretKey#Region(optional)#ProjectId(optional). The secret must have 2, 3 or 4 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "xftrans",
|
||||||
|
defaultSecret: "AppID#ApiSecret#ApiKey",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const parts = secret?.split("#");
|
||||||
|
const flag = parts.length === 3;
|
||||||
|
const partsInfo = `AppID: ${parts[0]}\nApiSecret: ${parts[1]}\nApiKey: ${parts[2]}`;
|
||||||
|
const source = getService("xftrans");
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status: flag && secret !== source.defaultSecret,
|
||||||
|
info:
|
||||||
|
secret === source.defaultSecret
|
||||||
|
? "The secret is not set."
|
||||||
|
: flag
|
||||||
|
? partsInfo
|
||||||
|
: `The secret format of Xftrans Domain Text Translation is AppID#ApiSecret#ApiKey. The secret must have 3 parts joined by '#', but got ${parts?.length}.\n${partsInfo}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sentence",
|
||||||
|
id: "gpt",
|
||||||
|
defaultSecret: "",
|
||||||
|
secretValidator(secret: string) {
|
||||||
|
const status = secret.length === 51 && /^sk-/.test(secret);
|
||||||
|
const empty = secret.length === 0;
|
||||||
|
return {
|
||||||
|
secret,
|
||||||
|
status,
|
||||||
|
info: empty
|
||||||
|
? "The secret is not set."
|
||||||
|
: status
|
||||||
|
? "Click the button to check connectivity."
|
||||||
|
: "Ths secret key format is invalid.",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "bingdict",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "haicidict",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "youdaodict",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "freedictionaryapi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "webliodict",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "word",
|
||||||
|
id: "collinsdict",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getService(id: string) {
|
||||||
|
return SERVICES[SERVICES.findIndex((service) => service.id === id)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LANG_CODE = <const>[
|
||||||
|
{ code: "af", name: "Afrikaans" },
|
||||||
|
{ code: "af-ZA", name: "Afrikaans (South Africa)" },
|
||||||
|
{ code: "ar", name: "Arabic" },
|
||||||
|
{ code: "ar-AE", name: "Arabic (U.A.E.)" },
|
||||||
|
{ code: "ar-BH", name: "Arabic (Bahrain)" },
|
||||||
|
{ code: "ar-DZ", name: "Arabic (Algeria)" },
|
||||||
|
{ code: "ar-EG", name: "Arabic (Egypt)" },
|
||||||
|
{ code: "ar-IQ", name: "Arabic (Iraq)" },
|
||||||
|
{ code: "ar-JO", name: "Arabic (Jordan)" },
|
||||||
|
{ code: "ar-KW", name: "Arabic (Kuwait)" },
|
||||||
|
{ code: "ar-LB", name: "Arabic (Lebanon)" },
|
||||||
|
{ code: "ar-LY", name: "Arabic (Libya)" },
|
||||||
|
{ code: "ar-MA", name: "Arabic (Morocco)" },
|
||||||
|
{ code: "ar-OM", name: "Arabic (Oman)" },
|
||||||
|
{ code: "ar-QA", name: "Arabic (Qatar)" },
|
||||||
|
{ code: "ar-SA", name: "Arabic (Saudi Arabia)" },
|
||||||
|
{ code: "ar-SY", name: "Arabic (Syria)" },
|
||||||
|
{ code: "ar-TN", name: "Arabic (Tunisia)" },
|
||||||
|
{ code: "ar-YE", name: "Arabic (Yemen)" },
|
||||||
|
{ code: "az", name: "Azeri (Latin)" },
|
||||||
|
{ code: "az-AZ", name: "Azeri (Latin) (Azerbaijan)" },
|
||||||
|
{ code: "az-AZ", name: "Azeri (Cyrillic) (Azerbaijan)" },
|
||||||
|
{ code: "be", name: "Belarusian" },
|
||||||
|
{ code: "be-BY", name: "Belarusian (Belarus)" },
|
||||||
|
{ code: "bg", name: "Bulgarian" },
|
||||||
|
{ code: "bg-BG", name: "Bulgarian (Bulgaria)" },
|
||||||
|
{ code: "bs-BA", name: "Bosnian (Bosnia and Herzegovina)" },
|
||||||
|
{ code: "ca", name: "Catalan" },
|
||||||
|
{ code: "ca-ES", name: "Catalan (Spain)" },
|
||||||
|
{ code: "cs", name: "Czech" },
|
||||||
|
{ code: "cs-CZ", name: "Czech (Czech Republic)" },
|
||||||
|
{ code: "cy", name: "Welsh" },
|
||||||
|
{ code: "cy-GB", name: "Welsh (United Kingdom)" },
|
||||||
|
{ code: "da", name: "Danish" },
|
||||||
|
{ code: "da-DK", name: "Danish (Denmark)" },
|
||||||
|
{ code: "de", name: "German" },
|
||||||
|
{ code: "de-AT", name: "German (Austria)" },
|
||||||
|
{ code: "de-CH", name: "German (Switzerland)" },
|
||||||
|
{ code: "de-DE", name: "German (Germany)" },
|
||||||
|
{ code: "de-LI", name: "German (Liechtenstein)" },
|
||||||
|
{ code: "de-LU", name: "German (Luxembourg)" },
|
||||||
|
{ code: "dv", name: "Divehi" },
|
||||||
|
{ code: "dv-MV", name: "Divehi (Maldives)" },
|
||||||
|
{ code: "el", name: "Greek" },
|
||||||
|
{ code: "el-GR", name: "Greek (Greece)" },
|
||||||
|
{ code: "en", name: "English" },
|
||||||
|
{ code: "en-AU", name: "English (Australia)" },
|
||||||
|
{ code: "en-BZ", name: "English (Belize)" },
|
||||||
|
{ code: "en-CA", name: "English (Canada)" },
|
||||||
|
{ code: "en-CB", name: "English (Caribbean)" },
|
||||||
|
{ code: "en-GB", name: "English (United Kingdom)" },
|
||||||
|
{ code: "en-IE", name: "English (Ireland)" },
|
||||||
|
{ code: "en-JM", name: "English (Jamaica)" },
|
||||||
|
{ code: "en-NZ", name: "English (New Zealand)" },
|
||||||
|
{ code: "en-PH", name: "English (Republic of the Philippines)" },
|
||||||
|
{ code: "en-TT", name: "English (Trinidad and Tobago)" },
|
||||||
|
{ code: "en-US", name: "English (United States)" },
|
||||||
|
{ code: "en-ZA", name: "English (South Africa)" },
|
||||||
|
{ code: "en-ZW", name: "English (Zimbabwe)" },
|
||||||
|
{ code: "eo", name: "Esperanto" },
|
||||||
|
{ code: "es", name: "Spanish" },
|
||||||
|
{ code: "es-AR", name: "Spanish (Argentina)" },
|
||||||
|
{ code: "es-BO", name: "Spanish (Bolivia)" },
|
||||||
|
{ code: "es-CL", name: "Spanish (Chile)" },
|
||||||
|
{ code: "es-CO", name: "Spanish (Colombia)" },
|
||||||
|
{ code: "es-CR", name: "Spanish (Costa Rica)" },
|
||||||
|
{ code: "es-DO", name: "Spanish (Dominican Republic)" },
|
||||||
|
{ code: "es-EC", name: "Spanish (Ecuador)" },
|
||||||
|
{ code: "es-ES", name: "Spanish (Castilian)" },
|
||||||
|
{ code: "es-ES", name: "Spanish (Spain)" },
|
||||||
|
{ code: "es-GT", name: "Spanish (Guatemala)" },
|
||||||
|
{ code: "es-HN", name: "Spanish (Honduras)" },
|
||||||
|
{ code: "es-MX", name: "Spanish (Mexico)" },
|
||||||
|
{ code: "es-NI", name: "Spanish (Nicaragua)" },
|
||||||
|
{ code: "es-PA", name: "Spanish (Panama)" },
|
||||||
|
{ code: "es-PE", name: "Spanish (Peru)" },
|
||||||
|
{ code: "es-PR", name: "Spanish (Puerto Rico)" },
|
||||||
|
{ code: "es-PY", name: "Spanish (Paraguay)" },
|
||||||
|
{ code: "es-SV", name: "Spanish (El Salvador)" },
|
||||||
|
{ code: "es-UY", name: "Spanish (Uruguay)" },
|
||||||
|
{ code: "es-VE", name: "Spanish (Venezuela)" },
|
||||||
|
{ code: "et", name: "Estonian" },
|
||||||
|
{ code: "et-EE", name: "Estonian (Estonia)" },
|
||||||
|
{ code: "eu", name: "Basque" },
|
||||||
|
{ code: "eu-ES", name: "Basque (Spain)" },
|
||||||
|
{ code: "fa", name: "Farsi" },
|
||||||
|
{ code: "fa-IR", name: "Farsi (Iran)" },
|
||||||
|
{ code: "fi", name: "Finnish" },
|
||||||
|
{ code: "fi-FI", name: "Finnish (Finland)" },
|
||||||
|
{ code: "fo", name: "Faroese" },
|
||||||
|
{ code: "fo-FO", name: "Faroese (Faroe Islands)" },
|
||||||
|
{ code: "fr", name: "French" },
|
||||||
|
{ code: "fr-BE", name: "French (Belgium)" },
|
||||||
|
{ code: "fr-CA", name: "French (Canada)" },
|
||||||
|
{ code: "fr-CH", name: "French (Switzerland)" },
|
||||||
|
{ code: "fr-FR", name: "French (France)" },
|
||||||
|
{ code: "fr-LU", name: "French (Luxembourg)" },
|
||||||
|
{ code: "fr-MC", name: "French (Principality of Monaco)" },
|
||||||
|
{ code: "gl", name: "Galician" },
|
||||||
|
{ code: "gl-ES", name: "Galician (Spain)" },
|
||||||
|
{ code: "gu", name: "Gujarati" },
|
||||||
|
{ code: "gu-IN", name: "Gujarati (India)" },
|
||||||
|
{ code: "he", name: "Hebrew" },
|
||||||
|
{ code: "he-IL", name: "Hebrew (Israel)" },
|
||||||
|
{ code: "hi", name: "Hindi" },
|
||||||
|
{ code: "hi-IN", name: "Hindi (India)" },
|
||||||
|
{ code: "hr", name: "Croatian" },
|
||||||
|
{ code: "hr-BA", name: "Croatian (Bosnia and Herzegovina)" },
|
||||||
|
{ code: "hr-HR", name: "Croatian (Croatia)" },
|
||||||
|
{ code: "hu", name: "Hungarian" },
|
||||||
|
{ code: "hu-HU", name: "Hungarian (Hungary)" },
|
||||||
|
{ code: "hy", name: "Armenian" },
|
||||||
|
{ code: "hy-AM", name: "Armenian (Armenia)" },
|
||||||
|
{ code: "id", name: "Indonesian" },
|
||||||
|
{ code: "id-ID", name: "Indonesian (Indonesia)" },
|
||||||
|
{ code: "is", name: "Icelandic" },
|
||||||
|
{ code: "is-IS", name: "Icelandic (Iceland)" },
|
||||||
|
{ code: "it", name: "Italian" },
|
||||||
|
{ code: "it-CH", name: "Italian (Switzerland)" },
|
||||||
|
{ code: "it-IT", name: "Italian (Italy)" },
|
||||||
|
{ code: "ja", name: "Japanese" },
|
||||||
|
{ code: "ja-JP", name: "Japanese (Japan)" },
|
||||||
|
{ code: "ka", name: "Georgian" },
|
||||||
|
{ code: "ka-GE", name: "Georgian (Georgia)" },
|
||||||
|
{ code: "kk", name: "Kazakh" },
|
||||||
|
{ code: "kk-KZ", name: "Kazakh (Kazakhstan)" },
|
||||||
|
{ code: "kn", name: "Kannada" },
|
||||||
|
{ code: "kn-IN", name: "Kannada (India)" },
|
||||||
|
{ code: "ko", name: "Korean" },
|
||||||
|
{ code: "ko-KR", name: "Korean (Korea)" },
|
||||||
|
{ code: "kok", name: "Konkani" },
|
||||||
|
{ code: "kok-IN", name: "Konkani (India)" },
|
||||||
|
{ code: "ky", name: "Kyrgyz" },
|
||||||
|
{ code: "ky-KG", name: "Kyrgyz (Kyrgyzstan)" },
|
||||||
|
{ code: "lt", name: "Lithuanian" },
|
||||||
|
{ code: "lt-LT", name: "Lithuanian (Lithuania)" },
|
||||||
|
{ code: "lv", name: "Latvian" },
|
||||||
|
{ code: "lv-LV", name: "Latvian (Latvia)" },
|
||||||
|
{ code: "mi", name: "Maori" },
|
||||||
|
{ code: "mi-NZ", name: "Maori (New Zealand)" },
|
||||||
|
{ code: "mk", name: "FYRO Macedonian" },
|
||||||
|
{
|
||||||
|
code: "mk-MK",
|
||||||
|
name: "FYRO Macedonian (Former Yugoslav Republic of Macedonia)",
|
||||||
|
},
|
||||||
|
{ code: "mn", name: "Mongolian" },
|
||||||
|
{ code: "mn-MN", name: "Mongolian (Mongolia)" },
|
||||||
|
{ code: "mr", name: "Marathi" },
|
||||||
|
{ code: "mr-IN", name: "Marathi (India)" },
|
||||||
|
{ code: "ms", name: "Malay" },
|
||||||
|
{ code: "ms-BN", name: "Malay (Brunei Darussalam)" },
|
||||||
|
{ code: "ms-MY", name: "Malay (Malaysia)" },
|
||||||
|
{ code: "mt", name: "Maltese" },
|
||||||
|
{ code: "mt-MT", name: "Maltese (Malta)" },
|
||||||
|
{ code: "nb", name: "Norwegian (Bokm?l)" },
|
||||||
|
{ code: "nb-NO", name: "Norwegian (Bokm?l) (Norway)" },
|
||||||
|
{ code: "nl", name: "Dutch" },
|
||||||
|
{ code: "nl-BE", name: "Dutch (Belgium)" },
|
||||||
|
{ code: "nl-NL", name: "Dutch (Netherlands)" },
|
||||||
|
{ code: "nn-NO", name: "Norwegian (Nynorsk) (Norway)" },
|
||||||
|
{ code: "ns", name: "Northern Sotho" },
|
||||||
|
{ code: "ns-ZA", name: "Northern Sotho (South Africa)" },
|
||||||
|
{ code: "pa", name: "Punjabi" },
|
||||||
|
{ code: "pa-IN", name: "Punjabi (India)" },
|
||||||
|
{ code: "pl", name: "Polish" },
|
||||||
|
{ code: "pl-PL", name: "Polish (Poland)" },
|
||||||
|
{ code: "ps", name: "Pashto" },
|
||||||
|
{ code: "ps-AR", name: "Pashto (Afghanistan)" },
|
||||||
|
{ code: "pt", name: "Portuguese" },
|
||||||
|
{ code: "pt-BR", name: "Portuguese (Brazil)" },
|
||||||
|
{ code: "pt-PT", name: "Portuguese (Portugal)" },
|
||||||
|
{ code: "qu", name: "Quechua" },
|
||||||
|
{ code: "qu-BO", name: "Quechua (Bolivia)" },
|
||||||
|
{ code: "qu-EC", name: "Quechua (Ecuador)" },
|
||||||
|
{ code: "qu-PE", name: "Quechua (Peru)" },
|
||||||
|
{ code: "ro", name: "Romanian" },
|
||||||
|
{ code: "ro-RO", name: "Romanian (Romania)" },
|
||||||
|
{ code: "ru", name: "Russian" },
|
||||||
|
{ code: "ru-RU", name: "Russian (Russia)" },
|
||||||
|
{ code: "sa", name: "Sanskrit" },
|
||||||
|
{ code: "sa-IN", name: "Sanskrit (India)" },
|
||||||
|
{ code: "se", name: "Sami (Northern)" },
|
||||||
|
{ code: "se-FI", name: "Sami (Northern) (Finland)" },
|
||||||
|
{ code: "se-FI", name: "Sami (Skolt) (Finland)" },
|
||||||
|
{ code: "se-FI", name: "Sami (Inari) (Finland)" },
|
||||||
|
{ code: "se-NO", name: "Sami (Northern) (Norway)" },
|
||||||
|
{ code: "se-NO", name: "Sami (Lule) (Norway)" },
|
||||||
|
{ code: "se-NO", name: "Sami (Southern) (Norway)" },
|
||||||
|
{ code: "se-SE", name: "Sami (Northern) (Sweden)" },
|
||||||
|
{ code: "se-SE", name: "Sami (Lule) (Sweden)" },
|
||||||
|
{ code: "se-SE", name: "Sami (Southern) (Sweden)" },
|
||||||
|
{ code: "sk", name: "Slovak" },
|
||||||
|
{ code: "sk-SK", name: "Slovak (Slovakia)" },
|
||||||
|
{ code: "sl", name: "Slovenian" },
|
||||||
|
{ code: "sl-SI", name: "Slovenian (Slovenia)" },
|
||||||
|
{ code: "sq", name: "Albanian" },
|
||||||
|
{ code: "sq-AL", name: "Albanian (Albania)" },
|
||||||
|
{ code: "sr-BA", name: "Serbian (Latin) (Bosnia and Herzegovina)" },
|
||||||
|
{ code: "sr-BA", name: "Serbian (Cyrillic) (Bosnia and Herzegovina)" },
|
||||||
|
{ code: "sr-SP", name: "Serbian (Latin) (Serbia and Montenegro)" },
|
||||||
|
{ code: "sr-SP", name: "Serbian (Cyrillic) (Serbia and Montenegro)" },
|
||||||
|
{ code: "sv", name: "Swedish" },
|
||||||
|
{ code: "sv-FI", name: "Swedish (Finland)" },
|
||||||
|
{ code: "sv-SE", name: "Swedish (Sweden)" },
|
||||||
|
{ code: "sw", name: "Swahili" },
|
||||||
|
{ code: "sw-KE", name: "Swahili (Kenya)" },
|
||||||
|
{ code: "syr", name: "Syriac" },
|
||||||
|
{ code: "syr-SY", name: "Syriac (Syria)" },
|
||||||
|
{ code: "ta", name: "Tamil" },
|
||||||
|
{ code: "ta-IN", name: "Tamil (India)" },
|
||||||
|
{ code: "te", name: "Telugu" },
|
||||||
|
{ code: "te-IN", name: "Telugu (India)" },
|
||||||
|
{ code: "th", name: "Thai" },
|
||||||
|
{ code: "th-TH", name: "Thai (Thailand)" },
|
||||||
|
{ code: "tl", name: "Tagalog" },
|
||||||
|
{ code: "tl-PH", name: "Tagalog (Philippines)" },
|
||||||
|
{ code: "tn", name: "Tswana" },
|
||||||
|
{ code: "tn-ZA", name: "Tswana (South Africa)" },
|
||||||
|
{ code: "tr", name: "Turkish" },
|
||||||
|
{ code: "tr-TR", name: "Turkish (Turkey)" },
|
||||||
|
{ code: "tt", name: "Tatar" },
|
||||||
|
{ code: "tt-RU", name: "Tatar (Russia)" },
|
||||||
|
{ code: "ts", name: "Tsonga" },
|
||||||
|
{ code: "uk", name: "Ukrainian" },
|
||||||
|
{ code: "uk-UA", name: "Ukrainian (Ukraine)" },
|
||||||
|
{ code: "ur", name: "Urdu" },
|
||||||
|
{ code: "ur-PK", name: "Urdu (Islamic Republic of Pakistan)" },
|
||||||
|
{ code: "uz", name: "Uzbek (Latin)" },
|
||||||
|
{ code: "uz-UZ", name: "Uzbek (Latin) (Uzbekistan)" },
|
||||||
|
{ code: "uz-UZ", name: "Uzbek (Cyrillic) (Uzbekistan)" },
|
||||||
|
{ code: "vi", name: "Vietnamese" },
|
||||||
|
{ code: "vi-VN", name: "Vietnamese (Viet Nam)" },
|
||||||
|
{ code: "xh", name: "Xhosa" },
|
||||||
|
{ code: "xh-ZA", name: "Xhosa (South Africa)" },
|
||||||
|
{ code: "zh", name: "Chinese" },
|
||||||
|
{ code: "zh-CN", name: "Chinese (S)" },
|
||||||
|
{ code: "zh-HK", name: "Chinese (Hong Kong)" },
|
||||||
|
{ code: "zh-MO", name: "Chinese (Macau)" },
|
||||||
|
{ code: "zh-SG", name: "Chinese (Singapore)" },
|
||||||
|
{ code: "zh-TW", name: "Chinese (T)" },
|
||||||
|
{ code: "zu", name: "Zulu" },
|
||||||
|
{ code: "zu-ZA", name: "Zulu (South Africa)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SVGIcon = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" width="16" height="16" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#64B5F6;}
|
||||||
|
.st1{fill:#1E88E5;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M4.4,11.1h1.4c0.1,0,0.2-0.1,0.1-0.2L5.2,8.7c0-0.1-0.2-0.1-0.3,0l-0.7,2.2C4.2,11,4.3,11.1,4.4,11.1L4.4,11.1
|
||||||
|
z M4.4,11.1"/>
|
||||||
|
<path class="st0" d="M8.8,5H1.4C0.6,5,0,5.7,0,6.4v8.2C0,15.4,0.6,16,1.4,16h7.4c0.8,0,1.4-0.6,1.4-1.4V6.4C10.2,5.7,9.5,5,8.8,5
|
||||||
|
L8.8,5z M7.9,14.2c-0.1,0.1-0.2,0.2-0.3,0.2c0,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.2,0C7,14.3,7,14.2,7,14.1l-0.6-1.9
|
||||||
|
C6.3,12,6.2,12,6.1,12H4c-0.1,0-0.1,0-0.2,0.1l-0.6,2c-0.1,0.1-0.1,0.2-0.3,0.3c-0.1,0.1-0.3,0.1-0.4,0.1c-0.2,0-0.3-0.1-0.3-0.2
|
||||||
|
c0-0.1-0.1-0.2,0-0.4l2.1-6.4c0.1-0.3,0.4-0.5,0.7-0.5h0c0.3,0,0.6,0.2,0.7,0.5l0,0l2.1,6.5C8,14,8,14.1,7.9,14.2L7.9,14.2z
|
||||||
|
M7.9,14.2"/>
|
||||||
|
<path class="st1" d="M14.3,0H7.5C6.6,0,5.8,0.8,5.8,1.7v2.1C5.8,4,6,4.1,6.1,4.1H8c0.3,0,0.5,0,0.7,0.1C8.6,3.9,8.6,3.7,8.5,3.4
|
||||||
|
H7.6C7.4,3.4,7.3,3.3,7.3,3c0-0.3,0.1-0.5,0.3-0.5h2.8c-0.1-0.3-0.2-0.5-0.2-0.7c0-0.2,0.1-0.4,0.3-0.5c0.3-0.1,0.4,0,0.6,0.2
|
||||||
|
c0,0.1,0.1,0.3,0.2,0.6c0.1,0.2,0.1,0.4,0.1,0.4h2.4c0.3,0,0.4,0.2,0.4,0.5c0,0.3-0.1,0.5-0.4,0.5h-0.6c-0.1,0-0.1,0-0.1,0
|
||||||
|
C12.8,4.9,12.3,6,11.6,7c0.6,0.5,1.3,0.9,2.3,1.3c0.3,0.1,0.3,0.3,0.3,0.6c-0.1,0.2-0.3,0.3-0.6,0.2c-0.9-0.3-1.8-0.8-2.5-1.3v2.9
|
||||||
|
c0,0.2,0.1,0.3,0.3,0.3h3c0.9,0,1.7-0.8,1.7-1.7V1.7C16,0.8,15.2,0,14.3,0L14.3,0z M14.3,0"/>
|
||||||
|
<path class="st1" d="M12,3.4H9.6c-0.1,0-0.2,0.1-0.1,0.2C9.6,4,9.7,4.4,9.9,4.8c0,0,0,0,0,0.1c0.4,0.3,0.7,0.8,0.9,1.2
|
||||||
|
c0.2,0,0.1,0,0.3,0c0.5-0.8,0.9-1.6,1.1-2.5C12.1,3.5,12.1,3.4,12,3.4L12,3.4z M12,3.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>`;
|
110
src/utils/crypto.ts
Normal file
110
src/utils/crypto.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
function base64(buffer: ArrayBuffer) {
|
||||||
|
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||||
|
return ztoolkit.getGlobal("btoa")(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex(buffer: ArrayBuffer) {
|
||||||
|
const hashArray = Array.from(new Uint8Array(buffer));
|
||||||
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hmacSha1Digest(stringToSign: string, secretKey: string) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
enc.encode(secretKey),
|
||||||
|
{
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-1",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
return crypto.subtle.sign("HMAC", key, enc.encode(stringToSign));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hmacSha256Digest(stringToSign: string, secretKey: string) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
enc.encode(secretKey),
|
||||||
|
{
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
return crypto.subtle.sign("HMAC", key, enc.encode(stringToSign));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256Digest(message: string) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
return crypto.subtle.digest("SHA-256", enc.encode(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pkcs7Pad(block: Uint8Array | Array<number>) {
|
||||||
|
const padding = 16 - block.length;
|
||||||
|
const pad = new Uint8Array(padding);
|
||||||
|
pad.fill(padding);
|
||||||
|
return new Uint8Array([...block, ...pad]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES ECB encrypt, use CBC mode to simulate ECB mode
|
||||||
|
async function aesEcbEncrypt(message: string, secret: string) {
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
new TextEncoder().encode(secret),
|
||||||
|
{
|
||||||
|
name: "AES-CBC",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["encrypt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const encodeStr = new TextEncoder().encode(message);
|
||||||
|
// split encoded string to 16 byte blocks
|
||||||
|
const blocks = [];
|
||||||
|
for (let i = 0; i < encodeStr.length; i += 16) {
|
||||||
|
const block = encodeStr.subarray(i, i + 16);
|
||||||
|
blocks.push(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blocks.length || blocks[blocks.length - 1].length === 16) {
|
||||||
|
blocks.push(pkcs7Pad([])); // pad empty block
|
||||||
|
} else {
|
||||||
|
blocks[blocks.length - 1] = pkcs7Pad(blocks[blocks.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt each block, do not pad
|
||||||
|
const zeros = new Uint8Array(16);
|
||||||
|
const encryptedBlocks = await Promise.all(
|
||||||
|
blocks.map((block) =>
|
||||||
|
crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: block,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
zeros
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// concatenate encrypted blocks
|
||||||
|
const encrypted = new Uint8Array(encryptedBlocks.length * 16);
|
||||||
|
let offset = 0;
|
||||||
|
for (const block of encryptedBlocks) {
|
||||||
|
encrypted.set(new Uint8Array(block).subarray(0, 16), offset);
|
||||||
|
offset += 16;
|
||||||
|
}
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
aesEcbEncrypt,
|
||||||
|
base64,
|
||||||
|
hex,
|
||||||
|
hmacSha1Digest,
|
||||||
|
hmacSha256Digest,
|
||||||
|
sha256Digest,
|
||||||
|
};
|
213
src/utils/gptModels.ts
Normal file
213
src/utils/gptModels.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { getPref, setPref } from "./prefs";
|
||||||
|
import { getString } from "./locale";
|
||||||
|
import { updateGPTModel } from "../modules/services/gpt";
|
||||||
|
|
||||||
|
export async function gptStatusCallback(status: boolean) {
|
||||||
|
const selectedModel = getPref("gptModel");
|
||||||
|
const dialog = new ztoolkit.Dialog(2, 1);
|
||||||
|
const dialogData: { [key: string | number]: any } = {
|
||||||
|
url: getPref("gptUrl"),
|
||||||
|
models: getPref("gptModel"),
|
||||||
|
temperature: parseFloat(getPref("gptTemperature") as string),
|
||||||
|
loadCallback: async () => {
|
||||||
|
const doc = dialog.window.document;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = await updateGPTModel();
|
||||||
|
// Due to an unknown bug with Zotero 7, the `<select>` element cannot be properly rendered.
|
||||||
|
// Toolkit uses a workaround to render the element, so please do not touch the original element and just replace its inner `<option>` elements.
|
||||||
|
// See https://groups.google.com/g/zotero-dev/c/iG763ZlWQ_U
|
||||||
|
const modelsSelect = doc.querySelector("#gptModels")!;
|
||||||
|
modelsSelect.innerHTML = "";
|
||||||
|
ztoolkit.UI.appendElement(
|
||||||
|
{
|
||||||
|
tag: "fragment",
|
||||||
|
children: models.map((model: string) => ({
|
||||||
|
tag: "option",
|
||||||
|
properties: {
|
||||||
|
value: model,
|
||||||
|
innerHTML: model,
|
||||||
|
selected: model === selectedModel,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
modelsSelect
|
||||||
|
);
|
||||||
|
|
||||||
|
doc.querySelector("#gptStatus")!.innerHTML = getString(
|
||||||
|
"service.gpt.dialog.status.available"
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
const HTTP = Zotero.HTTP;
|
||||||
|
let gptStatus = "unexpect";
|
||||||
|
|
||||||
|
if (error instanceof HTTP.TimeoutException) {
|
||||||
|
gptStatus = "timeout";
|
||||||
|
} else if (error.xmlhttp?.status === 401) {
|
||||||
|
gptStatus = "invalid";
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.querySelector("#gptStatus")!.innerHTML = getString(
|
||||||
|
`service.gpt.dialog.status.${gptStatus}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.setDialogData(dialogData)
|
||||||
|
.addCell(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
namespace: "html",
|
||||||
|
styles: {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 4fr",
|
||||||
|
rowGap: "10px",
|
||||||
|
columnGap: "5px",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
attributes: {
|
||||||
|
for: "url",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.gpt.dialog.url"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "input",
|
||||||
|
id: "gptUrl",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "url",
|
||||||
|
"data-prop": "value",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
attributes: {
|
||||||
|
for: "models",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.gpt.dialog.models"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "select",
|
||||||
|
id: "gptModels",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "models",
|
||||||
|
"data-prop": "value",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "option",
|
||||||
|
properties: {
|
||||||
|
value: selectedModel,
|
||||||
|
innerHTML: selectedModel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
attributes: {
|
||||||
|
for: "temperature",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.gpt.dialog.temperature"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "input",
|
||||||
|
id: "temperature",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "temperature",
|
||||||
|
"data-prop": "value",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
step: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
namespace: "html",
|
||||||
|
styles: {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 4fr 1fr",
|
||||||
|
rowGap: "5px",
|
||||||
|
columnGap: "5px",
|
||||||
|
marginTop: "10px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.gpt.dialog.status"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
id: "gptStatus",
|
||||||
|
styles: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.gpt.dialog.status.load"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "a",
|
||||||
|
styles: {
|
||||||
|
textDecoration: "none",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
href: "https://gist.github.com/GrayXu/f1b72353b4b0493d51d47f0f7498b67b",
|
||||||
|
innerHTML: getString("service.gpt.dialog.help"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addButton(getString("service.gpt.dialog.save"), "save")
|
||||||
|
.addButton(getString("service.gpt.dialog.close"), "close");
|
||||||
|
|
||||||
|
dialog.open(getString("service.gpt.dialog.title"));
|
||||||
|
|
||||||
|
await dialogData.unloadLock?.promise;
|
||||||
|
switch (dialogData._lastButtonId) {
|
||||||
|
case "save":
|
||||||
|
{
|
||||||
|
const temperature = dialogData.temperature;
|
||||||
|
|
||||||
|
if (temperature && temperature >= 0 && temperature <= 2) {
|
||||||
|
setPref("gptTemperature", dialogData.temperature.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
setPref("gptUrl", dialogData.url)
|
||||||
|
setPref("gptModel", dialogData.models);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
23
src/utils/locale.ts
Normal file
23
src/utils/locale.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
|
||||||
|
export function initLocale() {
|
||||||
|
addon.data.locale = {
|
||||||
|
stringBundle: Components.classes["@mozilla.org/intl/stringbundle;1"]
|
||||||
|
.getService(Components.interfaces.nsIStringBundleService)
|
||||||
|
.createBundle(`chrome://${config.addonRef}/locale/addon.properties`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getString(localeString: string): string {
|
||||||
|
switch (localeString) {
|
||||||
|
case "alt":
|
||||||
|
return Zotero.isMac ? "⌥" : "Alt";
|
||||||
|
case "ctrl":
|
||||||
|
return Zotero.isMac ? "⌘" : "Ctrl";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return addon.data.locale.stringBundle.GetStringFromName(localeString);
|
||||||
|
} catch (e) {
|
||||||
|
return localeString;
|
||||||
|
}
|
||||||
|
}
|
359
src/utils/niuTransLogin.ts
Normal file
359
src/utils/niuTransLogin.ts
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import { JSEncrypt } from "jsencrypt";
|
||||||
|
import { getString } from "./locale";
|
||||||
|
import { getPref, setPref } from "./prefs";
|
||||||
|
import { setServiceSecret } from "./translate";
|
||||||
|
|
||||||
|
export async function niutransStatusCallback(status: boolean) {
|
||||||
|
const dictLibList = getPref("niutransDictLibList") as string;
|
||||||
|
const memoryLibList = getPref("niutransMemoryLibList") as string;
|
||||||
|
const dictLibListObj = JSON.parse(dictLibList);
|
||||||
|
const memoryLibListObj = JSON.parse(memoryLibList);
|
||||||
|
const signInOrRefresh = status ? "refresh" : "signin";
|
||||||
|
const dialogData: { [key: string | number]: any } = {
|
||||||
|
username: getPref("niutransUsername"),
|
||||||
|
password: getPref("niutransPassword"),
|
||||||
|
dictLibList,
|
||||||
|
memoryLibList,
|
||||||
|
dictNo: getPref("niutransDictNo"),
|
||||||
|
memoryNo: getPref("niutransMemoryNo"),
|
||||||
|
beforeUnloadCallback: () => {
|
||||||
|
setPref("niutransUsername", dialog.dialogData.username);
|
||||||
|
setPref("niutransPassword", dialog.dialogData.password);
|
||||||
|
setPref("niutransDictLibList", dialog.dialogData.dictLibList);
|
||||||
|
setPref("niutransMemoryLibList", dialog.dialogData.memoryLibList);
|
||||||
|
setPref("niutransDictNo", dialog.dialogData.dictNo);
|
||||||
|
setPref("niutransMemoryNo", dialog.dialogData.memoryNo);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dialog = new ztoolkit.Dialog(5, 3)
|
||||||
|
.setDialogData(dialogData)
|
||||||
|
.addCell(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
attributes: {
|
||||||
|
for: "username",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.username"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(0, 1, {
|
||||||
|
tag: "input",
|
||||||
|
id: "username",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "username",
|
||||||
|
"data-prop": "value",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCell(
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
tag: "a",
|
||||||
|
properties: {
|
||||||
|
href: "https://niutrans.com/register",
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.signup"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
attributes: {
|
||||||
|
for: "password",
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.password"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(1, 1, {
|
||||||
|
tag: "input",
|
||||||
|
id: "password",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "password",
|
||||||
|
"data-prop": "value",
|
||||||
|
type: "password",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCell(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
tag: "a",
|
||||||
|
properties: {
|
||||||
|
href: "https://niutrans.com/password_find",
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.forget"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.dictLib"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
tag: "select",
|
||||||
|
id: "dictLib",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "dictNo",
|
||||||
|
"data-prop": "value",
|
||||||
|
},
|
||||||
|
children: dictLibListObj.map(
|
||||||
|
(dict: { dictName: string; dictNo: string }) => ({
|
||||||
|
tag: "option",
|
||||||
|
properties: {
|
||||||
|
value: dict.dictNo,
|
||||||
|
innerHTML: dict.dictName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
tag: "label",
|
||||||
|
namespace: "html",
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.memoryLib"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
tag: "select",
|
||||||
|
id: "memoryLib",
|
||||||
|
attributes: {
|
||||||
|
"data-bind": "memoryNo",
|
||||||
|
"data-prop": "value",
|
||||||
|
},
|
||||||
|
children: memoryLibListObj.map(
|
||||||
|
(memory: { memoryName: string; memoryNo: string }) => ({
|
||||||
|
tag: "option",
|
||||||
|
properties: {
|
||||||
|
value: memory.memoryNo,
|
||||||
|
innerHTML: memory.memoryName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.addCell(4, 0, {
|
||||||
|
tag: "div",
|
||||||
|
styles: {
|
||||||
|
width: "200px",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: "span",
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.tip0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "a",
|
||||||
|
properties: {
|
||||||
|
href: "https://niutrans.com/cloud/resource/index",
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.tip1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "span",
|
||||||
|
properties: {
|
||||||
|
innerHTML: getString("service.niutranspro.dialog.tip2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.addButton(
|
||||||
|
getString(`service.niutranspro.dialog.${signInOrRefresh}`),
|
||||||
|
"signin"
|
||||||
|
)
|
||||||
|
.addCell(4, 1, { tag: "fragment" }, false)
|
||||||
|
.addCell(4, 2, { tag: "fragment" }, false);
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
dialog.addButton(
|
||||||
|
getString("service.niutranspro.dialog.signout"),
|
||||||
|
"signout"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.addButton(getString("service.niutranspro.dialog.close"), "close")
|
||||||
|
.open(getString("service.niutranspro.dialog.title"));
|
||||||
|
|
||||||
|
await dialogData.unloadLock?.promise;
|
||||||
|
switch (dialogData._lastButtonId) {
|
||||||
|
case "signin":
|
||||||
|
{
|
||||||
|
const { loginFlag, loginErrorMessage } = await niutransLogin(
|
||||||
|
dialogData.username,
|
||||||
|
dialogData.password
|
||||||
|
);
|
||||||
|
if (!loginFlag) {
|
||||||
|
window.alert(loginErrorMessage);
|
||||||
|
}
|
||||||
|
await niutransStatusCallback(loginFlag);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "signout": {
|
||||||
|
{
|
||||||
|
setPref("niutransUsername", "");
|
||||||
|
setPref("niutransPassword", "");
|
||||||
|
setPref("niutransDictLibList", "[]");
|
||||||
|
setPref("niutransMemoryLibList", "[]");
|
||||||
|
setPref("niutransDictNo", "");
|
||||||
|
setPref("niutransMemoryNo", "");
|
||||||
|
setServiceSecret("niutranspro", "");
|
||||||
|
await niutransStatusCallback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function niutransLogin(username: string, password: string) {
|
||||||
|
let loginFlag = false;
|
||||||
|
let loginErrorMessage = "Not login";
|
||||||
|
let keyxhr = await getPublicKey();
|
||||||
|
if (keyxhr?.status === 200 && keyxhr.response.flag === 1) {
|
||||||
|
} else {
|
||||||
|
return { loginFlag, loginErrorMessage };
|
||||||
|
}
|
||||||
|
let encrypt = new JSEncrypt();
|
||||||
|
encrypt.setPublicKey(keyxhr.response.key);
|
||||||
|
let encryptionPassword = encrypt.encrypt(password);
|
||||||
|
encryptionPassword = encodeURIComponent(encryptionPassword);
|
||||||
|
const userLoginXhr = await loginApi(username, encryptionPassword);
|
||||||
|
if (userLoginXhr?.status === 200) {
|
||||||
|
if (userLoginXhr.response.flag === 1) {
|
||||||
|
let apikey = userLoginXhr.response.apikey;
|
||||||
|
setPref("niutransUsername", username);
|
||||||
|
setPref("niutransPassword", password);
|
||||||
|
setServiceSecret("niutranspro", apikey);
|
||||||
|
await setDictLibList(apikey);
|
||||||
|
await setMemoryLibList(apikey);
|
||||||
|
loginFlag = true;
|
||||||
|
} else {
|
||||||
|
loginFlag = false;
|
||||||
|
loginErrorMessage = userLoginXhr.response.msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { loginFlag, loginErrorMessage };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginApi(username: string, password: string) {
|
||||||
|
return await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://apis.niutrans.com/NiuTransAPIServer/checkInformation",
|
||||||
|
{
|
||||||
|
body: `account=${username}&encryptionPassword=${password}`,
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDictLibList(apikey: string) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://apis.niutrans.com/NiuTransAPIServer/getDictLibList",
|
||||||
|
{
|
||||||
|
body: `apikey=${apikey}`,
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (xhr?.status === 200 && xhr.response.flag !== 0) {
|
||||||
|
const dictList = xhr.response.dlist as {
|
||||||
|
dictName: string;
|
||||||
|
dictNo: string;
|
||||||
|
isUse: number;
|
||||||
|
}[];
|
||||||
|
const dictNo = dictList.find((dict) => dict.isUse === 1)?.dictNo || "";
|
||||||
|
if (dictNo && !getPref("niutransDictNo")) {
|
||||||
|
setPref("niutransDictNo", dictNo);
|
||||||
|
}
|
||||||
|
setPref(
|
||||||
|
"niutransDictLibList",
|
||||||
|
JSON.stringify(
|
||||||
|
dictList.map((dict) => ({
|
||||||
|
dictName: dict.dictName,
|
||||||
|
dictNo: dict.dictNo,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setMemoryLibList(apikey: string) {
|
||||||
|
const xhr = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
"https://apis.niutrans.com/NiuTransAPIServer/getMemoryLibList",
|
||||||
|
{
|
||||||
|
body: `apikey=${apikey}`,
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xhr?.status === 200 && xhr.response.flag !== 0) {
|
||||||
|
const memoryList = xhr.response.mlist as {
|
||||||
|
memoryName: string;
|
||||||
|
memoryNo: string;
|
||||||
|
isUse: number;
|
||||||
|
}[];
|
||||||
|
const memoryNo =
|
||||||
|
memoryList.find((memory) => memory.isUse === 1)?.memoryNo || "";
|
||||||
|
if (memoryNo && !getPref("niutransMemoryNo")) {
|
||||||
|
setPref("niutransMemoryNo", memoryNo);
|
||||||
|
}
|
||||||
|
setPref(
|
||||||
|
"niutransMemoryLibList",
|
||||||
|
JSON.stringify(
|
||||||
|
memoryList.map((memory) => ({
|
||||||
|
memoryName: memory.memoryName,
|
||||||
|
memoryNo: memory.memoryNo,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicKey() {
|
||||||
|
return await Zotero.HTTP.request(
|
||||||
|
"GET",
|
||||||
|
"https://apis.niutrans.com/NiuTransAPIServer/getpublickey",
|
||||||
|
{
|
||||||
|
responseType: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
13
src/utils/prefs.ts
Normal file
13
src/utils/prefs.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { config } from "../../package.json";
|
||||||
|
|
||||||
|
export function getPref(key: string) {
|
||||||
|
return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPref(key: string, value: string | number | boolean) {
|
||||||
|
return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPref(key: string) {
|
||||||
|
return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true);
|
||||||
|
}
|
20
src/utils/str.ts
Normal file
20
src/utils/str.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function slice(str: string, len: number) {
|
||||||
|
return str.length > len ? `${str.slice(0, len - 3)}...` : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fill(
|
||||||
|
str: string,
|
||||||
|
len: number,
|
||||||
|
options: { char: string; position: "start" | "end" } = {
|
||||||
|
char: " ",
|
||||||
|
position: "end",
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (str.length >= len) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str[options.position === "start" ? "padStart" : "padEnd"](
|
||||||
|
len - str.length,
|
||||||
|
options.char
|
||||||
|
);
|
||||||
|
}
|
354
src/utils/translate.ts
Normal file
354
src/utils/translate.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
import { getService, SecretValidateResult, SERVICES } from "./config";
|
||||||
|
import { gptStatusCallback } from "./gptModels";
|
||||||
|
import { getString } from "./locale";
|
||||||
|
import { niutransStatusCallback } from "./niuTransLogin";
|
||||||
|
import { getPref, setPref } from "./prefs";
|
||||||
|
|
||||||
|
export interface TranslateTask {
|
||||||
|
/**
|
||||||
|
* Task id.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Task type.
|
||||||
|
*/
|
||||||
|
type: "text" | "annotation" | "title" | "abstract" | "custom";
|
||||||
|
/**
|
||||||
|
* Raw text for translation.
|
||||||
|
*/
|
||||||
|
raw: string;
|
||||||
|
/**
|
||||||
|
* Translation result or error info.
|
||||||
|
*/
|
||||||
|
result: string;
|
||||||
|
/**
|
||||||
|
* Audio resources.
|
||||||
|
*/
|
||||||
|
audio: { text: string; url: string }[];
|
||||||
|
/**
|
||||||
|
* Service id.
|
||||||
|
*/
|
||||||
|
service: string;
|
||||||
|
/**
|
||||||
|
* Candidate service ids.
|
||||||
|
*
|
||||||
|
* Only used when the run of `service` fails.
|
||||||
|
* Generally this is for the fallback of word services.
|
||||||
|
*/
|
||||||
|
candidateServices: string[];
|
||||||
|
/**
|
||||||
|
* Zotero item id.
|
||||||
|
*
|
||||||
|
* For language disable check.
|
||||||
|
*/
|
||||||
|
itemId: number | undefined;
|
||||||
|
/**
|
||||||
|
* From language
|
||||||
|
*
|
||||||
|
* Generated at task runtime.
|
||||||
|
*/
|
||||||
|
langfrom?: string;
|
||||||
|
/**
|
||||||
|
* To language.
|
||||||
|
*
|
||||||
|
* Generated at task runtime.
|
||||||
|
*/
|
||||||
|
langto?: string;
|
||||||
|
/**
|
||||||
|
* Service secret.
|
||||||
|
*
|
||||||
|
* Generated at task runtime.
|
||||||
|
*/
|
||||||
|
secret?: string;
|
||||||
|
/**
|
||||||
|
* task status.
|
||||||
|
*/
|
||||||
|
status: "waiting" | "processing" | "success" | "fail";
|
||||||
|
/**
|
||||||
|
* Extra tasks.
|
||||||
|
*
|
||||||
|
* For extra services function.
|
||||||
|
*/
|
||||||
|
extraTasks: TranslateTask[] & { extraTasks: [] }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TranslateTaskProcessor = (
|
||||||
|
data: Required<TranslateTask>
|
||||||
|
) => Promise<void> | void;
|
||||||
|
|
||||||
|
export class TranslateTaskRunner {
|
||||||
|
protected processor: TranslateTaskProcessor;
|
||||||
|
constructor(processor: TranslateTaskProcessor) {
|
||||||
|
this.processor = processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(data: TranslateTask) {
|
||||||
|
data.langfrom = getPref("sourceLanguage") as string;
|
||||||
|
data.langto = getPref("targetLanguage") as string;
|
||||||
|
data.secret = getServiceSecret(data.service);
|
||||||
|
data.status = "processing";
|
||||||
|
try {
|
||||||
|
ztoolkit.log(data);
|
||||||
|
await this.processor(data as Required<TranslateTask>);
|
||||||
|
data.status = "success";
|
||||||
|
} catch (e) {
|
||||||
|
data.result = this.makeErrorInfo(data.service, String(e));
|
||||||
|
data.status = "fail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected makeErrorInfo(serviceId: string, detail: string) {
|
||||||
|
return `${getString("service.errorPrefix")} ${getString(
|
||||||
|
`service.${serviceId}`
|
||||||
|
)}\n\n${detail}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTranslateTask(
|
||||||
|
raw: string,
|
||||||
|
itemId?: number,
|
||||||
|
type?: TranslateTask["type"],
|
||||||
|
service?: string
|
||||||
|
) {
|
||||||
|
if (!raw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
type = type || "text";
|
||||||
|
// Filter raw string
|
||||||
|
raw = raw.replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ").normalize("NFKC");
|
||||||
|
|
||||||
|
// Append raw text to last task's raw if in concat mode
|
||||||
|
const isConcatMode =
|
||||||
|
type === "text" &&
|
||||||
|
(addon.data.translate.concatCheckbox || addon.data.translate.concatKey);
|
||||||
|
const lastTask = getLastTranslateTask({ type: "text" });
|
||||||
|
if (isConcatMode && lastTask) {
|
||||||
|
lastTask.raw += " " + raw;
|
||||||
|
lastTask.extraTasks.forEach((extraTask) => (extraTask.raw += " " + raw));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create a new task
|
||||||
|
const newTask: TranslateTask = {
|
||||||
|
id: `${Zotero.Utilities.randomString()}-${new Date().getTime()}`,
|
||||||
|
type,
|
||||||
|
raw,
|
||||||
|
result: "",
|
||||||
|
audio: [],
|
||||||
|
service: "",
|
||||||
|
candidateServices: [],
|
||||||
|
itemId,
|
||||||
|
status: "waiting",
|
||||||
|
extraTasks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
setDefaultService(newTask);
|
||||||
|
} else {
|
||||||
|
newTask.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
addon.data.translate.queue.push(newTask);
|
||||||
|
// In case window panel requires extra translations
|
||||||
|
if (
|
||||||
|
type === "text" &&
|
||||||
|
addon.data.panel.windowPanel &&
|
||||||
|
!addon.data.panel.windowPanel.closed
|
||||||
|
) {
|
||||||
|
(getPref("extraEngines") as string)
|
||||||
|
.split(",")
|
||||||
|
.filter((s) => s)
|
||||||
|
.forEach((extraService) =>
|
||||||
|
newTask.extraTasks.push({
|
||||||
|
id: `${Zotero.Utilities.randomString()}-${new Date().getTime()}`,
|
||||||
|
type: "text",
|
||||||
|
raw,
|
||||||
|
result: "",
|
||||||
|
audio: [],
|
||||||
|
service: extraService,
|
||||||
|
candidateServices: [],
|
||||||
|
extraTasks: [],
|
||||||
|
itemId,
|
||||||
|
status: "waiting",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Keep queue size
|
||||||
|
cleanTasks();
|
||||||
|
return newTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTranslateAnnotationTask(itemId: number) {
|
||||||
|
const item = Zotero.Items.get(itemId);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return addTranslateTask(item.annotationText, item.id, "annotation");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTranslateTitleTask(
|
||||||
|
itemId: number,
|
||||||
|
skipIfExists: boolean = false
|
||||||
|
) {
|
||||||
|
const item = Zotero.Items.get(itemId);
|
||||||
|
if (
|
||||||
|
item?.isRegularItem() &&
|
||||||
|
!(
|
||||||
|
skipIfExists &&
|
||||||
|
ztoolkit.ExtraField.getExtraField(item, "titleTranslation")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return addTranslateTask(item.getField("title") as string, item.id, "title");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTranslateAbstractTask(
|
||||||
|
itemId: number,
|
||||||
|
skipIfExists: boolean = false
|
||||||
|
) {
|
||||||
|
const item = Zotero.Items.get(itemId);
|
||||||
|
if (
|
||||||
|
item?.isRegularItem() &&
|
||||||
|
!(
|
||||||
|
skipIfExists &&
|
||||||
|
ztoolkit.ExtraField.getExtraField(item, "abstractTranslation")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return addTranslateTask(
|
||||||
|
item.getField("abstractNote") as string,
|
||||||
|
item.id,
|
||||||
|
"abstract"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultService(task: TranslateTask) {
|
||||||
|
// Use wordService(dictSource) for single word translation
|
||||||
|
if (
|
||||||
|
getPref("enableDict") &&
|
||||||
|
task.raw.trim().split(/[^a-z,A-Z]+/).length == 1
|
||||||
|
) {
|
||||||
|
task.service = getPref("dictSource") as string;
|
||||||
|
task.candidateServices.push(getPref("translateSource") as string);
|
||||||
|
} else {
|
||||||
|
task.service = getPref("translateSource") as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case service is still empty
|
||||||
|
task.service = task.service || SERVICES[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanTasks() {
|
||||||
|
if (
|
||||||
|
addon.data.translate.queue.length > addon.data.translate.maximumQueueLength
|
||||||
|
) {
|
||||||
|
addon.data.translate.queue.splice(
|
||||||
|
0,
|
||||||
|
Math.floor(addon.data.translate.maximumQueueLength / 3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslateTasks(count: number) {
|
||||||
|
return addon.data.translate.queue.slice(-count);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastTranslateTask<
|
||||||
|
K extends keyof TranslateTask,
|
||||||
|
V extends TranslateTask[K]
|
||||||
|
>(conditions?: { [key in K]: V }) {
|
||||||
|
const queue = addon.data.translate.queue;
|
||||||
|
let i = queue.length - 1;
|
||||||
|
while (i >= 0) {
|
||||||
|
const currentTask = queue[i];
|
||||||
|
const notMatchConditions =
|
||||||
|
conditions &&
|
||||||
|
Object.keys(conditions)
|
||||||
|
.map((key) => currentTask[key as K] === conditions[key as K])
|
||||||
|
.includes(false);
|
||||||
|
if (!notMatchConditions) {
|
||||||
|
return queue[i];
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putTranslateTaskAtHead(taskId: string) {
|
||||||
|
const queue = addon.data.translate.queue;
|
||||||
|
const idx = queue.findIndex((task) => task.id === taskId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const targetTask = queue.splice(idx, 1)[0];
|
||||||
|
queue.push(targetTask);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServiceSecret(serviceId: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(getPref("secretObj") as string)[serviceId] || "";
|
||||||
|
} catch (e) {
|
||||||
|
setPref("secretObj", "{}");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setServiceSecret(serviceId: string, secret: string) {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
secrets = JSON.parse(getPref("secretObj") as string) || {};
|
||||||
|
} catch (e) {
|
||||||
|
secrets = {};
|
||||||
|
}
|
||||||
|
secrets[serviceId] = secret;
|
||||||
|
setPref("secretObj", JSON.stringify(secrets));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateServiceSecret(
|
||||||
|
serviceId: string,
|
||||||
|
validateCallback?: (result: SecretValidateResult) => void
|
||||||
|
): SecretValidateResult {
|
||||||
|
const secret = getServiceSecret(serviceId);
|
||||||
|
const validator = getService(serviceId).secretValidator;
|
||||||
|
if (!validator) {
|
||||||
|
return { secret, status: true, info: "" };
|
||||||
|
}
|
||||||
|
const validateResult = validator(secret);
|
||||||
|
if (validateCallback) {
|
||||||
|
validateCallback(validateResult);
|
||||||
|
}
|
||||||
|
return validateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const secretStatusButtonData: {
|
||||||
|
[key: string]: {
|
||||||
|
labels: { [_k in "pass" | "fail"]: string };
|
||||||
|
callback(status: boolean): void;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
niutranspro: {
|
||||||
|
labels: {
|
||||||
|
pass: "service.niutranspro.secret.pass",
|
||||||
|
fail: "service.niutranspro.secret.fail",
|
||||||
|
},
|
||||||
|
callback: niutransStatusCallback,
|
||||||
|
},
|
||||||
|
deeplcustom: {
|
||||||
|
labels: {
|
||||||
|
pass: "service.deeplcustom.secret.pass",
|
||||||
|
fail: "service.deeplcustom.secret.fail",
|
||||||
|
},
|
||||||
|
callback: function () {
|
||||||
|
Zotero.launchURL(
|
||||||
|
"https://github.com/KyleChoy/zotero-pdf-translate/blob/CustomDeepL/README.md"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gpt: {
|
||||||
|
labels: {
|
||||||
|
pass: "service.gpt.secret.pass",
|
||||||
|
fail: "service.gpt.secret.fail",
|
||||||
|
},
|
||||||
|
callback: gptStatusCallback,
|
||||||
|
},
|
||||||
|
};
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2017",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src", "typing", "node_modules/zotero-types"],
|
||||||
|
"exclude": ["builds", "addon"]
|
||||||
|
}
|
21
typing/global.d.ts
vendored
Normal file
21
typing/global.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
declare const _globalThis: {
|
||||||
|
[key: string]: any;
|
||||||
|
Zotero: _ZoteroTypes.Zotero;
|
||||||
|
ZoteroPane: _ZoteroTypes.ZoteroPane;
|
||||||
|
Zotero_Tabs: typeof Zotero_Tabs;
|
||||||
|
ZoteroContextPane: typeof ZoteroContextPane;
|
||||||
|
window: Window;
|
||||||
|
document: Document;
|
||||||
|
crypto: Crypto;
|
||||||
|
TextEncoder: typeof TextEncoder;
|
||||||
|
ztoolkit: typeof ztoolkit;
|
||||||
|
addon: typeof addon;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const ztoolkit: import("../src/addon").ZToolkit;
|
||||||
|
|
||||||
|
declare const rootURI: string;
|
||||||
|
|
||||||
|
declare const addon: import("../src/addon").default;
|
||||||
|
|
||||||
|
declare const __env__: "production" | "development";
|
26
update-template.json
Normal file
26
update-template.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"addons": {
|
||||||
|
"__addonID__": {
|
||||||
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "__buildVersion__",
|
||||||
|
"update_link": "__releasepage__",
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"strict_min_version": "60.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "__buildVersion__",
|
||||||
|
"update_link": "__releasepage__",
|
||||||
|
"applications": {
|
||||||
|
"zotero": {
|
||||||
|
"strict_min_version": "6.999"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
update-template.rdf
Normal file
30
update-template.rdf
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<rdf:Description rdf:about="urn:mozilla:extension:__addonID__">
|
||||||
|
<em:updates>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:version>__buildVersion__</em:version>
|
||||||
|
<em:targetApplication>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||||
|
<em:minVersion>5.999</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
<em:updateLink>__releasepage__</em:updateLink>
|
||||||
|
</rdf:Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
<em:targetApplication>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:id>juris-m@juris-m.github.io</em:id>
|
||||||
|
<em:minVersion>5.999</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
<em:updateLink>__releasepage__</em:updateLink>
|
||||||
|
</rdf:Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</em:updates>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
26
update.json
Normal file
26
update.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"addons": {
|
||||||
|
"zoteropdftranslate@euclpts.com": {
|
||||||
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "1.0.25",
|
||||||
|
"update_link": "https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi",
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"strict_min_version": "60.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.25",
|
||||||
|
"update_link": "https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi",
|
||||||
|
"applications": {
|
||||||
|
"zotero": {
|
||||||
|
"strict_min_version": "6.999"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
update.rdf
Normal file
30
update.rdf
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<rdf:Description rdf:about="urn:mozilla:extension:zoteropdftranslate@euclpts.com">
|
||||||
|
<em:updates>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:version>1.0.25</em:version>
|
||||||
|
<em:targetApplication>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||||
|
<em:minVersion>5.999</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
<em:updateLink>https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi</em:updateLink>
|
||||||
|
</rdf:Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
<em:targetApplication>
|
||||||
|
<rdf:Description>
|
||||||
|
<em:id>juris-m@juris-m.github.io</em:id>
|
||||||
|
<em:minVersion>5.999</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
<em:updateLink>https://github.com/windingwind/zotero-pdf-translate/releases/latest/download/zotero-pdf-translate.xpi</em:updateLink>
|
||||||
|
</rdf:Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</em:updates>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
Loading…
x
Reference in New Issue
Block a user