init: 1.0.0a

This commit is contained in:
windingwind 2023-04-10 18:21:17 +08:00
parent 939b348041
commit b5bcc6d880
162 changed files with 10025 additions and 12148 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
**/builds
node_modules
.vscode
package-lock.json
zotero-cmd.json

38
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,38 @@
{
// 使 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
View 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}",
"});"
]
}
}

149
LICENSE
View File

@ -1,23 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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 General Public License is a free, copyleft license for
software and other kinds of works.
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,
the GNU General Public License is intended to guarantee your freedom to
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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
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.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
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.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
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.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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.
@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"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.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
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 Affero General Public License into a single
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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
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 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
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 General
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 General Public License, you may choose any version ever published
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 General Public License can be used, that proxy's
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.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
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 General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
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 the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

180
README.md
View File

@ -1,180 +0,0 @@
# Zotero Better Notes
![teaser](./image/README/teaser.png)
Everything about note management. All in Zotero.
Better Notes Handbook: [EN](https://github.com/windingwind/zotero-better-notes/wiki) | [中文 (provide translation)](https://zotero.yuque.com/staff-gkhviy/better-notes/biigg4?)
User Guide: [EN](./UserGuide.md) | [中文](./UserGuideCN.md)
## Introduction
Better Notes is a plugin for [Zotero](https://zotero.org).
It streamlines your unordered workflows of metadata analyzing, paper reading, annotating, and note-taking into a closed loop in Zotero.
Works out of the box and is highly customizable.
## Contents
- [Quick start](#quick-start)
- [Install](#install)
- [Note workspace](#note-workspace)
- Connect notes with the [bi-directional links](#bi-directional-link-support)
- Automate note generation with the [note templates](#note-templates)
- [Export notes](#export) to different formats
- Integrate with 3rd-party MarkDown editors seamlessly with [note<->markdown syncing](#syncing-note-markdown)
- [Other features](#other-features)
## Quick Start
> See [Handbook:Quick Start](https://zotero.yuque.com/staff-gkhviy/better-notes/gw5d7v) for more details.
New to note-taking? Install and start now!
Already an Obsidian/Logseq/... user? Forget those complicated integration tools and keep them in sync with MarkDown files with just one click.
## Install
- Download the latest release (.xpi file) from the [Releases Page](https://github.com/windingwind/zotero-better-notes/releases)_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 plugin is now listed.
## Note Workspace
> See [Handbook:Workspace](https://zotero.yuque.com/staff-gkhviy/better-notes/yul2qm) for more details.
The workspace allows you to focus on the note, as shown in the teaser on top of the README.
- Note outline(tree view, mindmap, and bubble map)
- Note links Preview
## Bi-directional Link
> See [Handbook:Bi-directional Link](https://zotero.yuque.com/staff-gkhviy/better-notes/yxpiew) for more details.
The bi-directional link note(双链笔记) is supported. Link your notes inside Zotero with just one click.
Export with its' linked sub-notes to Obsidian:
![Obsidian example](./image/README/markdown-ob.png)
## Note Templates
> See [Handbook:Note Templates](https://zotero.yuque.com/staff-gkhviy/better-notes/un54wc) for more details.
Use customized templates to import data from items/notes!
![template](./image/README/template.gif)
[How to Use Templates](./TemplateUsage.md)
[How to Write Your Own Template](./TemplateDoc)
See what templates can do and find templates contributed by the community here: [Note Templates from Community](https://github.com/windingwind/zotero-better-notes/discussions/categories/note-templates)
## Export
> See [Handbook:Export](https://zotero.yuque.com/staff-gkhviy/better-notes/nxlngg) for more details.
- To new note in Zotero
- To MarkDown file(embed or link, with images)
- To MS Word document(.docx)
- To PDF document(.pdf)
- To FreeMind file(.mm)
## Syncing: Note<->MarkDown
> See [Handbook:Sycn](https://zotero.yuque.com/staff-gkhviy/better-notes/aid2c3) for more details.
It's painless to Better Notes into your current workflow if you are using software like Obsidian. Keep your notes in sync with external MD files with one click.
Click 'Auto Sync to Export Path' the first time you export your note. Do not require any third-party tools or complicated setups!
Any modification in the note or its corresponding MarkDown file will be automatically synced.
![syncing](./image/README/sync.png)
## Other Features
> See [Handbook:Other Features](https://zotero.yuque.com/staff-gkhviy/better-notes/sh4v2y) for more details.
- Quick Note: annotation to note with one click. Support MarkDown comments.
<img src="./image/README/markdowncomment.png" width="400px"></img>
- Auto-insert new annotations to note. Disabled by default.
- Format MarkDown/AsciiDoc in the clipboard.
- Quick Cite: cite items in the note with the given cite format.
- Image annotation math OCR.
- Resize images (right-click on images).
- Preview images (double-click/ctrl-click on images).
- Customize link actions.
- Note editor enhancements.
- Quick switch main note.
- Copy note link.
- Import from MarkDown.
- Quick Cite.
- ...
## Development & Contributing
This addon is built based on the [Zotero Addon Template](https://github.com/windingwind/zotero-addon-template).
### Build
```shell
# A release-it command: version increase, npm run build, git push, and GitHub release
# You need to set the environment variable GITHUB_TOKEN https://github.com/settings/tokens
# release-it: https://github.com/release-it/release-it
npm run release
```
Alternatively, build it directly using build.js: `npm run build`
### Debug
1. Copy the Zotero command line config file. Modify the commands.
```sh
cp zotero-cmd-default.json zotero-cmd.json
```
2. Initialize the addon development environment following this [link](https://www.zotero.org/support/dev/client_coding/plugin_development#setting_up_a_plugin_development_environment).
3. Build the addon and restart Zotero with this npm command.
```sh
npm run restart
```
You can also debug code in these ways:
- Test code segments in Tools->Developer->Run Javascript;
- Debug output with `Zotero.debug()`. Find the outputs in Help->Debug Output Logging->View Output;
- UI debug. Zotero is built on the Firefox XUL framework. Debug XUL UI with software like [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer).
> XUL Documents:
> https://www.xul.fr/tutorial/
> http://www.xulplanet.com/
## Disclaimer
Use this code under AGPL (open source required). No warranties are provided. Keep the laws of your locality in mind!
Part of the code of this repo refers to other open-source projects within the allowed scope.
- [zotero-pdf-translate](https://github.com/windingwind/zotero-pdf-translate)
## My Other Zotero Addons
- [zotero-pdf-preview](https://github.com/windingwind/zotero-pdf-preview) PDF preview for Zotero
- [zotero-pdf-translate](https://github.com/windingwind/zotero-pdf-translate) PDF translation for Zotero 6
- [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!

View File

@ -1,25 +0,0 @@
# Note Template
## Introduction
Note Template is an advanced feature that allows users insert anything into the note.
You can easily reuse your note segments by making them a template.
Moreover, you can use `Javascript` code to generate custom contents based on the input!
![template](image/README/template.gif)
## Documentations
For new users, please see [Usage Documentation](TemplateUsage.md). It will guide you to know how to use templates.
If you want to write your own template, you can either add a new template from an existing note, or follow this [Template Documentation](TemplateDoc.md).
## Template Market
See [[Documentation] Note Templates from Community](https://github.com/windingwind/zotero-better-notes/issues/85)
Here are templates provided by other Zotero Better Notes users. Search to find one that suits your need!
If you don't know how to make a template, it is welcomed to discuss or ask for help in a new issue.

View File

@ -1,495 +0,0 @@
# How to Write Your Own Template
This documentation is like a dictionary. For beginners, see [How to Use Templates](./TemplateUsage.md).
Use `Ctrl+F` to look up what you need and make your own template!
## Stages
Some type of templates(Item) support stages.
Code wrapped inside the stage will be called at a specific time.
For example, the `beforeloop` stage code:
```js
// @beforeloop-begin
code;
// @beforeloop-end
```
| stage | calling time |
| ---------- | --------------------------------- |
| beforeloop | before entering the loop of items |
| default | loop of items |
| afterloop | after leaving the loop of items |
In other type of templates, the default stage is called.
## Variables
### QuickInsert
> variables: link: string, subNoteItem, noteItem
### QuickBackLink
> variables: subNoteItem, noteItem
### QuickImport
> variables: subNoteLines: string[], subNoteItem, noteItem
### QuickNoteV3
> variables: annotationItem, topItem
### ExportMDFileName
> variables: noteItem
### ExportMDFileHeader
> variables: noteItem
The return value must be a JSON string.
### Text
> variables: -
### Item
> beforeloop stage: items, copyNoteImage, editor, sharedObj(for temporary variables, shared by all stages)
> default stage: topItem, itemNotes, copyNoteImage, editor, sharedObj
> afterloop stage: items, copyNoteImage, editor, sharedObj
### Note
> Removed after v0.7.15+
## Formats
### Line
Description: Normal line.
Template Type: None.
Required Variable: None.
```html
<p>Your Line Here</p>
```
### Heading
Description: From h1 to h6.
Template Type: None.
Required Variable: None.
```html
<h1>Your Heding 1 Here</h1>
<h2>Your Heding 2 Here</h2>
```
### Highlight
Description: Highlight.
Template Type: None.
Required Variable: None.
```html
<p style="background-color:#dbeedd;">Text</p>
```
### Strong
Description: **Strong text**. Put it in a `p`.
Template Type: None.
Required Variable: None.
```html
<strong>Your Text</strong>
```
### Underline
Description: Underline text. Put it in a `p`.
Template Type: None.
Required Variable: None.
```html
<u>Your Text</u>
```
### Deleteline
Description: Deleteline text. Put it in a `p`.
Template Type: None.
Required Variable: None.
```html
<span style="text-decoration: line-through">Your Text</span>
```
### Link
Description: Link. Put it in a `p`.
Template Type: None.
Required Variable: None.
```html
<a href="zotero://replace/this/link">Link Text</a>
```
### Number List
Description: Number List.
Template Type: None.
Required Variable: None.
```html
<ol>
<li>First</li>
<li>Second</li>
<li>...</li>
</ol>
```
### Bullet List
Description: Bullet List.
Template Type: None.
Required Variable: None.
```html
<ul>
<li>First</li>
<li>Second</li>
<li>...</li>
</ul>
```
### Sup & Sub Text
Description: $\text{The}^{sup} \ and \ \text{the}_{sub}$ text.
Template Type: None.
Required Variable: None.
```html
<p>The<sup>sup</sup>and the<sub>sub</sub> text</p>
```
### Block Quote
Description:
> Block Quote
Template Type: None.
Required Variable: None.
```html
<blockquote>
<p>Text</p>
</blockquote>
```
### Monospaced
Description: `Monospaced`
Template Type: None.
Required Variable: None.
```html
<pre>Text</pre>
```
### Table
Description:
| Table | Column1 | Column2 |
| ---- | ---- | ---- |
| 00 | 01 | 02 |
| 10 | 11 | 12 |
Template Type: None.
Required Variable: None.
```html
<table>
<tr>
<th style="background-color:#dbeedd;">
<p style="text-align: right">Table</p>
</th>
<th style="background-color:#dbeedd;">Column1</th>
<th style="background-color:#dbeedd;">Column2</th>
</tr>
<tr>
<td>00</td>
<td>01</td>
<td>02</td>
</tr>
<tr>
<td>10</td>
<td>11</td>
<td>12</td>
</tr>
</table>
```
## General Fields
### Current Date
Description: Current Date.
Required Variable: None.
```js
<p>${new Date().toLocaleDateString()}</p>
```
### Tags
Description: `item.getTags()` returns tags in list. Usually use `tags.includes("YourTag") ? do sth : do sth else`.
Required Variable: any item/note/annotation.
```js
<p>${topItem.getTags()}</p>
```
## Note Fields
### Note Title
Description: Note Title. First line of note.
Template Type: Note.
Required Variable: noteItem/subNoteItem.
```js
<p>${noteItem.getNoteTitle()}</p>
```
### Note Content
Description: Note Content.
Template Type: Note.
Required Variable: noteItem/subNoteItem.
```js
<p>${noteItem.getNote()}</p>
```
### Note Link
Description: Note Link.
Template Type: Note.
Required Variable: noteItem/subNoteItem.
```js
<p>
<a href="${Zotero.BetterNotes.knowledge.getNoteLink(noteItem)}">
${noteItem.key}
</a>
</p>
```
## Item Fields
### Title
Description: Item title.
Template Type: Item.
Required Variable: topItem.
```js
<p>${topItem.getField("title")}</p>
```
### Publisher/Journal
Description: Publisher/Journal.
Template Type: Item.
Required Variable: topItem.
```js
<p>
$
{(() => {
if (topItem.itemType === "conferencePaper") {
return;
topItem.getField("conferenceName") ||
topItem.getField("proceedingsTitle");
}
if (topItem.itemType === "journalArticle")
return topItem.getField("publicationTitle");
if (topItem.itemType === "report") return topItem.getField("institution");
return topItem.getField("publicationTitle");
})()}
</p>
```
### Authors
Description: Authors.
Template Type: Item.
Required Variable: topItem.
```js
<p>
$
{topItem
.getCreators()
.map((v) => v.firstName + " " + v.lastName)
.join("; ")}
</p>
```
### Pub. date
Description: Pub. date.
Template Type: Item.
Required Variable: topItem.
```js
<p>${topItem.getField("date")}</p>
```
### Pub. date
Description: Publication date/time.
Template Type: Item.
Required Variable: topItem.
```js
<p>${topItem.getField("date")}</p>
```
### DOI
Description: DOI.
Template Type: Item.
Required Variable: topItem.
```js
<p>
<a href="https://doi.org/${topItem.getField('DOI')}">
${topItem.getField("DOI")}
</a>
</p>
```
### URL
Description: URL.
Template Type: Item.
Required Variable: topItem.
```js
<p>
<a href="${topItem.getField('url')}">${topItem.getField("url")}</a>
</p>
```
### CitationKey
Description: CitationKey.
Template Type: Item.
Required Variable: topItem.
```js
<p>${topItem.citationKey ? topItem.citationKey : ""}</p>
```
### PDF Link
Description: URL.
Template Type: Item.
Required Variable: topItem.
```js
<p>
$
{((topItem) => {
const getPDFLink = (_item) => {
let libraryID = _item.libraryID;
let library = Zotero.Libraries.get(libraryID);
let itemKey = _item.key;
let itemLink = "";
if (library.libraryType === "user") {
itemLink = `zotero://open-pdf/library/items/${itemKey}`;
} else if (library.libraryType === "group") {
itemLink = `zotero://open-pdf/groups/${library.id}/items/${itemKey}`;
}
return `<a href="${itemLink}">${_item.getFilename()}</a>`;
};
return Zotero.Items.get(topItem.getAttachments())
.filter((i) => i.isPDFAttachment())
.map((i) => getPDFLink(i))
.join("\n");
})(topItem)}
</p>
```
### Item Notes
Description: Walk all sub notes under an item. You can add your code inside the loop.
Template Type: Item.
Required Variable: topItem.
```js
${itemNotes.map((noteItem)=>{
// process each item note
const noteLine = `<blockquote>
${noteItem.getNote()}
</blockquote>`;
copyNoteImage(noteItem);
return noteLine;
}).join("\n")}
```
### About Item Fields
The `noteItem` and `topItem` is a Zotero Item object. The general data can be accessed using the `getField()` method.
For example: `topItem.getField('title')` will return the title of the `topItem`.
```ts
// Get Item Fields
getField: (void)=>string;
// Get Authors
getCreators: (void)=>{
fieldMode: number,
firstName: string, // may be empty
lastName: string,
creatorTypeID: number,
}[];
```
Find available fields of the selected item with the code below:
```js
const item = ZoteroPane.getSelectedItems().shift();
const usedFields = item.getUsedFields();
Zotero.ItemFields.getAll()
.filter((e) => usedFields.indexOf(e.id) >= 0)
.map((e) => e.name);
```
The result is like this (depending on the item you select):
```JSON
[
"0": "title"
"1": "date"
"2": "language"
"3": "shortTitle"
"4": "libraryCatalog"
"5": "url"
"6": "accessDate"
"7": "pages"
"8": "conferenceName"
]
```
or see https://aurimasv.github.io/z2csl/typeMap.xml for the detailed Zotero fields documentation.

View File

@ -1,42 +0,0 @@
# How to Use Templates
This documentation is for beginners.
If you want to customize your own template, see [How to Write Your Own Template](./TemplateDoc.md). Post an issue if you need help.
You can find templates under the Workspace Tab/Window->Edit:
![image](https://user-images.githubusercontent.com/33902321/169189492-ab27b5ef-d6b2-4e4b-9035-2c11a91d53a1.png)
## Add a template
Click the _Edit Templates_ to open the editor.
![image](https://user-images.githubusercontent.com/33902321/169189605-e450702c-2336-463f-b157-600a198d987c.png)
Create a blank template, or from a note. Here are example templates for the test:
Copy The Name & template text to a new template and save it!
**Template Name must include one of these keywords:**
### Custom Templates
These templates can have different names. The keyword must be incluede in the template name.
- Text: indicate it's a normal template
- Item: indicate it's an item template. Must select items before inserting
### System Templates
Only the template with specific name will be called.
- QuickInsert: Called when inserting a note link to main note.
- QuickBackLink: Called when inserting a note link to main note. The template will be inserted to the end of the sub-note and point to the main note by default.
- QuickImport: Called when importing a sub-note to main note.
- QuickNoteV3: Called when creating a note from an annotation.
> QuickNote is deprecated since v0.6.25
- ExportMDFileName: Called when exporting notes to markdown in batch/linked notes to markdown mode. The rendered template will be file name.
- ExportMDFileHeader: Called when exporting notes to markdown in batch/linked notes to markdown mode. The rendered template will be YAML header.
## Template Examples
Welcome to share & contribute your template! See [Templates From Community](https://github.com/windingwind/zotero-better-notes/discussions/categories/note-templates) for more templates.

View File

@ -1,193 +0,0 @@
# Zotero Better Notes User Guide: Workflow
Welcome to **Zotero Better Notes** !
This note helps you quickly learn how to use this addon in 3 min!
Let's start now.
## 1 What is Knowledge
Knowledge is an extension of Zotero's built-in note function.
Zotero's note is much like a markdown/rich-text editor. You can edit the format with the tools above⬆.
### 1.1 Workspace Window
The knowledge workspace window contains an outline area(left side⬅), the main note area, and the preview area(right side➡).
![workspace](./image/README/workspace.png)
Open workspace by clicking the 'Open Workspace' line above the 'My Library' line in Zotero main window.
![Open workspace](./image/README/openworkspace.png)
Alternatively, open it with the '🏠home' button on the top-left of note editors.
![main page](./image/README/mainpage.png)
### 1.2 Main note
This addon uses a Zotero note item as the main note. It will show up on the main area of the workspace window.
All links will be added to the main note.
Set a note as the main note in the right-click popup menu.
### 1.3 Workspace Menu
**File menu**:
- Create new main note(Ctrl/Cmd+N)
- Open main note(Ctrl/Cmd+O)
- Import notes(Ctrl/Cmd+I)
- Export notes(Ctrl/Cmd+E)
- Close workspace window(Ctrl/Cmd+W)
**Edit menu**:
- Insert heading
- Decrease selected heading level(Tab)
- Increase selected heading level(Shift+Tab)
**View menu**:
- Outline Views
**Help menu**:
- Open user guide
## 2 Gather Ideas to Main Note
### 2.1 From Note
Select a note outside the workspace window(in Zotero items view or PDF viewer), you may realize a button with the addon's icon on the top of the note editor toolbar.
Click it, the current note link will be inserted into the main note's cursor position;
Select a heading, and the note's link will be inserted into the end of this heading.
![insert-link-from-note](./image/README/from-note.png)
> **💡 Try it now!**
>
> Open a PDF and open/create a note(in the right side bar of PDF viewer). Add a link below.
### 2.2 From Annotation (including highlight and picture)
You can find a button with the addon's icon on every annotation(in the left sidebar of PDF viewer).
![insert-link-from-annotation](./image/README/from-annotation.png)
Click it, and a new note containing this annotation will be created under the PDF item. Annotation tags are copied to the new note.
You can then add its link to the main note in the note editor.
> **💡 Try it now!**
>
> Open a PDF and open/create an annotation(in the left sidebar of PDF viewer).
### 2.3 Import Existing Notes
You can import notes into the main note.
- In workspace window, File -> Import Notes;
- Select notes and confirm.
Notes' links will be appended to the end of the main note.
## 3 Check Linked Notes in Workspace Window
### 3.1 View Linked Notes
Suppose you have added a lot of links to the main note. Now, it's time to view what you've got.
Go back to the workspace window.
Open a note link, the linked note will show up in the preview area(right side➡).
![preview-link](./image/README/preview-note.png)
> **💡 Try it now!**
>
> Open a note link.
### 3.2 View Linked Note's PDF
Click the '📄PDF' button on the top-left of the preview area.
![openpdf](./image/README/openpdf.png)
> **💡 Try it now!**
>
> Open a linked note's PDF.
### 3.3 Insert Citation
Click the "Insert Citation" button in the middle of the preview area.
![insertCitation](./image/README/insertCitation.png)
Currently, the quick copy format must be a citation format, otherwise, this function will not work. See Edit->Preferences->Export in Zotero main window.
### 3.4 Import Linked Note
Import the selected linked note's context to the main note by clicking the 'import note' button in the link popup.
![importnoteandupdate](./image/README/importnoteandupdate.png)
Example:
![insertnote](./image/README/insertnote.png)
> **💡 Try it now!**
>
> Import one linked note.
### 3.5 Update Note Link Text
Update the link text in main note by clicking the 'update link text' button in the link popup.
> **💡 Try it now!**
>
> Modify the first line of a linked note in the preview area. Then update its link text in the main note.
## 4 Outline Mode
Switch the outline mode with the '📊mode' button on the bottom of the outline area.
![outline](./image/README/outline.png)
> **💡 Try it now!**
>
> Try different outline modes.
## 5 Export
Click the '⬆export' button on the top-right of the main note area. Choose a format to export, i.e. MarkDown.
![export](./image/README/export.png)
If you are using MS Word/OneNote, export to clipboard and paste there.
> **💡 Try it now!**
>
> Export this main note!
## 6 Start a New Job
After the export, you may want to start a new job with a new empty main note.
Create a note and right-click to set it as the main note, or just create a new main note.
Open different main note in workspace window->File->Open Main Note.
> **✨ Hint**
>
> Create a new collection and save all main notes there is the best way to manage them.
>
> The user guide should have done this for you.
Congratulations!
You can select a new main note and start working with **Zotero Better Notes** now. Have fun!

View File

@ -1,192 +0,0 @@
# Zotero Better Notes 用户指引:工作流
欢迎使用 **Zotero Better Notes** !
本笔记帮助您在 3 分钟内快速学习如何使用此插件!
现在开始吧。
## 1 认识 Knowledge
Knowledge 是 Zotero 内置 note 功能的扩展。
Zotero 的 note 很像一个标记/富文本编辑器。您可以使用上方工具编辑格式 ⬆️。
### 1.1 工作区窗口
知识工作区窗口包含一个大纲区域(左侧 ⬅️),主笔记区域和预览区域(右侧 ➡️)。
![workspace](./image/README/workspace.png)
在 Zotero 主窗口中单击“我的文库”上方的“Open Workspace”来打开工作区。
![Open workspace](./image/README/openworkspace.png)
或者,用笔记编辑器左上角的“🏠 主页”按钮。
![main page](./image/README/mainpage.png)
### 1.2 主笔记
这个插件使用某一个 Zotero note 作为主笔记。它将显示在工作区窗口的主笔记区域。
所有链接都将添加到主笔记中。
从右键菜单中设置当前一个笔记为主笔记:
### 1.3 工作区菜单栏
**文件菜单**
- 创建新的主笔记(Ctrl/Cmd+N)
- 打开主笔记(Ctrl/Cmd+O)
- 导入笔记(Ctrl/Cmd+I)
- 导出笔记(Ctrl/Cmd+E)
- 关闭(Ctrl/Cmd+W)
**编辑菜单**
- 插入标题
- 降低标题层级
- 提升标题层级
**查看菜单**
- 大纲视图
**帮助菜单**
- 打开用户指引
## 2 在主笔记中收集想法
### 2.1 从 Note
在工作区窗口外选择一个 note在 Zotero 条目视图或 PDF 阅读器中),您会在笔记编辑器工具栏顶部看到一个带有本插件图标的按钮。
点击它,当前笔记的链接将插入主笔记的光标位置;
选择一个标题层级,笔记的链接将插入该标题的末尾。
![insert-link-from-note](./image/README/from-note.png)
> **💡 尝试一下!**
>
> 打开 PDF 并打开/创建笔记(在 PDF 阅读器的右侧栏中)。用上面的方法在这条主笔记添加一个链接。
### 2.2 从 Annotation高亮批注和图片
你可以在每个批注上找到一个带有插件图标的按钮(在 PDF 阅读器的左侧栏中)。
![insert-link-from-annotation](./image/README/from-annotation.png)
单击它PDF条目下将创建一个带有此批注的新笔记。批注的标签将会复制到新的笔记。
然后可以在打开的笔记编辑器中将该笔记链接添加到主笔记。
> **💡 尝试一下!**
>
> 打开 PDF 并打开/创建批注高亮(在 PDF 阅读器的左侧栏中)。用上面的方法在这条主笔记添加一个链接。
### 2.3 从功能区导入笔记
支持从现有的多个笔记批量导入到主笔记中。
- 在工作区窗口菜单栏文件->导入笔记;
- 选择多个笔记并确认。
## 3 查看工作区窗口中的链接笔记
### 3.1 查看链接笔记
假设你已经在主笔记添加了很多的链接。现在,是时候看看你的结果了。
返回工作区窗口。
打开笔记链接,链接的笔记将显示在预览区域(右侧 ➡️)。
![preview-link](./image/README/preview-note.png)
> **💡 尝试一下!**
>
> 在工作区窗口打开一个笔记链接。
### 3.2 查看链接笔记的 PDF
在上一步打开的预览笔记中点击预览区左上角的“📄PDF”按钮。
![openpdf](./image/README/openpdf.png)
> **💡 尝试一下!**
>
> 打开一个链接笔记的 PDF。
### 3.3 插入当前子笔记父条目文献的引用格式
在上一步打开的预览笔记中,点击预览区中间的“插入引用”按钮。
![insertCitation](./image/README/insertCitation.png)
目前快速复制格式需要设置为citation format否则该功能无法正常工作。可在Zotero主窗口的编辑->首选项->导出中设置。
### 3.4 插入链接的子笔记
点击链接弹窗中的'import note'来插入选择的笔记内容。
![importnoteandupdate](./image/README/importnoteandupdate.png)
插入之后:
![insertnote](./image/README/insertnote.png)
> **💡 尝试一下!**
>
> 插入一个链接的笔记
### 3.5 更新链接文本
点击链接弹窗中的'update link text'来更新选择的笔记链接文本。
> **💡 尝试一下!**
>
> 在预览区域中编辑某个链接笔记的第一行文本,然后在主笔记区域更新它的链接文本。
## 4 大纲视图
点击大纲区域左下角的 ‘📊 大纲模式‘ 按钮 来切换大纲视图模式。
![outline](./image/README/outline.png)
> **💡 尝试一下!**
>
> 尝试不同的大纲模式(思维导图)
## 5 导出
点击主笔记区域右上角的“⬆️ 导出”按钮,或者工作区窗口菜单的文件->导出主笔记。选择要导出的格式,比如 MarkDown。
![export](./image/README/export.png)
如果您使用的是 MS Word/OneNote请导出到剪贴板并粘贴到那里。
> **💡 尝试一下!**
>
> 导出这个主笔记!
## 6 开始新的任务
导出后,您可能希望使用新的空主笔记开始新任务。
创建一个笔记,然后在右键菜单中将其设置为主笔记;或者直接创建一个新的主笔记。
使用工作区窗口菜单的文件->打开主笔记以切换不同的主笔记。
> **✨ 提示**
>
> 创建一个新的文件夹并在其中专门保存所有的主笔记——这是管理主笔记的最佳方法。
>
> 用户指引应该已经为您做到了这一点。
恭喜!
你现在可以选择或新建一个主笔记,然后开始使用 **Zotero Better Notes**了。用的开心!

153
addon/bootstrap.js vendored Normal file
View File

@ -0,0 +1,153 @@
/**
* 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.
* The `_globalThis` is the global root variable of the plugin sandbox environment
* and all child variables assigned to it is globally accessible.
* See `src/index.ts` for details.
*/
const ctx = {
rootURI,
document: Zotero.getMainWindow().document,
};
ctx._globalThis = ctx;
Services.scriptloader.loadSubScript(
`${rootURI}/chrome/content/scripts/index.js`,
ctx
);
}
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);
}

View File

@ -1,6 +1,3 @@
content __addonRef__ chrome/content/
skin __addonRef__ default chrome/skin/default/__addonRef__/
locale __addonRef__ en-US chrome/locale/en-US/
locale __addonRef__ zh-CN chrome/locale/zh-CN/
overlay chrome://zotero/content/zoteroPane.xul chrome://__addonRef__/content/overlay.xul

View File

@ -1,40 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/about.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % AboutDTD SYSTEM "chrome://zotero/locale/about.dtd">
%AboutDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<dialog id="zotero-better-notes-about" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" orient="vertical" buttons="accept" buttonlabelaccept="&zotero.about.close;" ondialogaccept="return true;">
<script src="chrome://zotero/content/include.js" />
<vbox id="aboutcontent">
<hbox>
<div xmlns="http://www.w3.org/1999/xhtml">
<img id="logo" src="chrome://__addonRef__/skin/favicon.png" />
</div>
<label id="name" value="__addonName__" style="font-size: 22px; font-weight: bold; color: #f2ac46; padding: 5px;" />
</hbox>
<hbox>
<label id="version" value="&zotero.__addonRef__.help.version.label;" />
<label id="build" value="&zotero.__addonRef__.help.releasetime.label;" />
</hbox>
<hbox>
<label id="feedback" value="&zotero.__addonRef__.help.feedback.caption.label;" />
<hbox style="display: block">
<label value="&zotero.__addonRef__.help.feedback.label;" class="zotero-text-link" onclick="Zotero.launchURL('__homepage__');" />
</hbox>
</hbox>
<hbox>
<label id="doc" value="&zotero.__addonRef__.help.doc.caption.label;" />
<hbox style="display: block">
<label value="&zotero.__addonRef__.help.doc.label;" class="zotero-text-link" onclick="Zotero.launchURL('https://zotero.yuque.com/books/share/f3fe159f-956c-4f10-ade3-c87559cacb60/biigg4');" />
</hbox>
</hbox>
</vbox>
</dialog>

View File

@ -120,7 +120,12 @@
),
$(
go.TextBlock,
{ textAlign: "center", maxSize: new go.Size(100, NaN) },
{
textAlign: "center",
maxSize: new go.Size(100, NaN),
editable: true,
textEdited: editNode,
},
new go.Binding("text", "text").makeTwoWay(),
new go.Binding("stroke", "stroke")
)
@ -191,12 +196,13 @@
node.parent.model.id === -1 ? 999 : node.parent.model.id;
nodeDataArray.push({
key: node.model.id,
text: `${node.model.rank === 7 ? "🔗" : ""}${wrapText(
text: `${node.model.level === 7 ? "🔗" : ""}${wrapText(
node.model.name
)}`,
stroke: "-moz-DialogText",
lineIndex: node.model.lineIndex,
noteLink: node.model.rank === 7 ? node.model.link : "",
level: node.model.level,
noteLink: node.model.level === 7 ? node.model.link : "",
});
linkDataArray.push({
from: parent,
@ -214,7 +220,7 @@
var olddata = oldnode.data;
if (olddata.noteLink) {
window.parent.postMessage(
{ type: "jumpNote", link: olddata.noteLink, id: olddata.key },
{ type: "openNote", link: olddata.noteLink, id: olddata.key },
"*"
);
} else {
@ -223,17 +229,37 @@
type: "jumpNode",
lineIndex: olddata.lineIndex,
id: olddata.key,
workspaceType: window.workspaceType || "tab",
},
"*"
);
}
}
function editNode(textBlock, previousText, currentText) {
const adorn = textBlock.part;
const data = adorn.data;
if (data.level === 7) {
alert("Link cannot be edited in mind map");
return false;
}
window.parent.postMessage(
{
type: "editNode",
lineIndex: data.lineIndex,
text: data.text,
workspaceType: window.workspaceType || "tab",
},
"*"
);
}
function handler(e) {
console.log(e);
switch (e.data.type) {
case "setMindMapData":
setData(e.data.nodes);
window.workspaceType = e.data.workspaceType;
break;
case "saveImage":
const imgString = Diagram.makeImageData({

View File

@ -1,97 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % ZoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%ZoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<dialog id="betternotes-export-dialog" windowtype="betternotes-export"
title="&zotero.__addonRef__.export.title;" orient="vertical" width="300" height="300"
buttons="cancel,accept" ondialogaccept="Zotero.BetterNotes.NoteExportWindow.doAccept();"
onload="Zotero.BetterNotes.NoteExportWindow.doLoad(window);"
onunload="Zotero.BetterNotes.NoteExportWindow.doUnload();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml" style="padding:2em"
persist="screenX screenY width height">
<script src="chrome://zotero/content/include.js" />
<vbox flex="1">
<groupbox flex="1">
<caption label="&zotero.__addonRef__.export.option.label;"></caption>
<rows flex="1">
<row>
<checkbox id="embedLink"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.singlefile.enable.label;"
checked="true"
oncommand="Zotero.BetterNotes.NoteExportWindow.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.link.enable.label;"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.singlefile.enable.label;" />
</row>
<row>
<checkbox id="exportNote" checked="false" />
<label value="&zotero.__addonRef__.export.note.enable.label;" />
</row>
</rows>
</groupbox>
<groupbox flex="1">
<caption label="&zotero.__addonRef__.export.markdown.label;"></caption>
<rows flex="1">
<row>
<checkbox id="exportMD" checked="true"
oncommand="Zotero.BetterNotes.NoteExportWindow.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.file.enable.label;" />
</row>
<row>
<checkbox id="exportSubMD"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.link.enable.label;"
checked="false"
oncommand="Zotero.BetterNotes.NoteExportWindow.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.singlefile.enable.label;"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.link.enable.label;" />
</row>
<row>
<checkbox id="exportYAMLHeader"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.yaml.enable.label;"
checked="true"
oncommand="Zotero.BetterNotes.NoteExportWindow.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.yaml.enable.label;"
tooltiptext="&zotero.__addonRef__.export.cannotworkwith.label;&zotero.__addonRef__.export.link.enable.label;" />
</row>
<row>
<checkbox id="exportAutoSync"
tooltiptext="&zotero.__addonRef__.export.workwith.label;&zotero.__addonRef__.export.singlefile.enable.label;"
checked="false"
oncommand="Zotero.BetterNotes.NoteExportWindow.doUpdate(event)" />
<label value="&zotero.__addonRef__.export.enableautosync.enable.label;"
tooltiptext="Only work with &zotero.__addonRef__.export.singlefile.enable.label;" />
</row>
</rows>
</groupbox>
<groupbox flex="1">
<caption label="&zotero.__addonRef__.export.document.label;"></caption>
<rows flex="1">
<row>
<checkbox id="exportDocx" checked="false" />
<label value="&zotero.__addonRef__.export.docx.enable.label;" />
</row>
<row>
<checkbox id="exportPDF" checked="false" />
<label value="&zotero.__addonRef__.export.pdf.enable.label;" />
</row>
</rows>
</groupbox>
<groupbox flex="1">
<caption label="&zotero.__addonRef__.export.mindmap.label;"></caption>
<rows flex="1">
<row>
<checkbox id="exportFreeMind" checked="false" />
<label value="&zotero.__addonRef__.export.freemind.enable.label;" />
</row>
</rows>
</groupbox>
</vbox>
</dialog>

View File

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
<title>Image Preview</title>
<meta charset="utf-8">
</head>
<body>
<style>
@ -11,6 +12,7 @@
width: 100%;
padding: 0;
margin: 0;
font-family: initial;
}
#image {
position: relative;
@ -35,7 +37,7 @@
align-items: center;
}
</style>
<link rel="stylesheet" href="chrome://__addonRef__/skin/workspace.css" />
<link rel="stylesheet" href="./tooltip.css" />
<script>
window.addEventListener("DOMContentLoaded", (e) => {
document.querySelector(".container").style["line-height"] = `${

File diff suppressed because one or more lines are too long

View File

@ -50,9 +50,7 @@
name: "TEXT",
minSize: new go.Size(30, 15),
editable: true,
textEdited: function (textBlock, previousText, currentText) {
console.log("editing heading", textBlock, currentText);
},
textEdited: editNode,
},
// remember not only the text string but the scale and the font in the node data
new go.Binding("text", "text").makeTwoWay(),
@ -264,13 +262,14 @@
node.parent.model.id === -1 ? 999 : node.parent.model.id;
data.nodeDataArray.push({
key: node.model.id,
text: `${node.model.rank === 7 ? "🔗" : ""}${wrapText(
text: `${node.model.level === 7 ? "🔗" : ""}${wrapText(
node.model.name
)}`,
stroke: "-moz-DialogText",
parent: parent,
lineIndex: node.model.lineIndex,
noteLink: node.model.rank === 7 ? node.model.link : "",
level: node.model.level,
noteLink: node.model.level === 7 ? node.model.link : "",
brush: go.Brush.randomColor(),
});
}
@ -283,7 +282,7 @@
var olddata = oldnode.data;
if (olddata.noteLink) {
window.parent.postMessage(
{ type: "jumpNote", link: olddata.noteLink, id: olddata.key },
{ type: "openNote", link: olddata.noteLink, id: olddata.key },
"*"
);
} else {
@ -292,17 +291,37 @@
type: "jumpNode",
lineIndex: olddata.lineIndex,
id: olddata.key,
workspaceType: window.workspaceType || "tab",
},
"*"
);
}
}
function editNode(textBlock, previousText, currentText) {
const adorn = textBlock.part;
const data = adorn.data;
if (data.level === 7) {
alert("Link cannot be edited in mind map");
return false;
}
window.parent.postMessage(
{
type: "editNode",
lineIndex: data.lineIndex,
text: data.text,
workspaceType: window.workspaceType || "tab",
},
"*"
);
}
function handler(e) {
console.log(e);
switch (e.data.type) {
case "setMindMapData":
setData(e.data.nodes);
window.workspaceType = e.data.workspaceType;
break;
case "saveImage":
const imgString = Diagram.makeImageData({

View File

@ -1,107 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://__addonRef__/skin/overlay.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % standaloneDTD SYSTEM "chrome://zotero/locale/standalone.dtd">
%standaloneDTD;
<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://zotero/locale/mozilla/editMenuOverlay.dtd">
%editMenuOverlayDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<overlay id="__addonRef__" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://__addonRef__/content/scripts/index.js" />
<keyset id="mainKeyset">
<key id="key_new_betternotes" class="key-type-betternotes" key="N" modifiers="accel" command="cmd_new_betternotes" />
<key id="key_open_betternotes" class="key-type-betternotes" key="O" modifiers="accel" command="cmd_open_betternotes" />
<key id="key_export_betternotes" class="key-type-betternotes" key="E" modifiers="accel" command="cmd_export_betternotes" />
<key id="key_sync_betternotes" class="key-type-betternotes" key="S" modifiers="accel,shift" command="cmd_sync_betternotes" />
</keyset>
<commandset id="mainCommandSet">
<command id="cmd_new_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'createWorkspace'});" />
<command id="cmd_open_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'selectMainNote'});" />
<command id="cmd_open_betternotesWindow" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openWorkspaceInWindow'});" />
<command id="cmd_export_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'export', content: {editorInstance: {_item: false}}});" />
<command id="cmd_sync_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'sync'});" />
<command id="cmd_sync_manager_betternotes" oncommand="Zotero.BetterNotes.SyncListWindow.openSyncList();" />
<command id="cmd_editTemplate_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'editTemplate'});" />
<!-- <command id="cmd_importlink_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'importLink'});" />
<command id="cmd_updatelink_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'updateLink'});" /> -->
<command id="cmd_autoannotation_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'updateAutoAnnotation'});" />
<command id="cmd_convertmd_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'convertMD'});" />
<command id="cmd_convertasciidoc_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'convertAsciiDoc'});" />
<command id="cmd_treeview_betternotes" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(1);" />
<command id="cmd_mindmap_betternotes" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(2);" />
<command id="cmd_bubblemap_betternotes" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(3);" />
<command id="cmd_guide_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openUserGuide'});" />
<command id="cmd_about_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openAbout'});" />
</commandset>
<popup id="zotero-itemmenu">
<menuseparator />
<menuitem id="zotero-itemmenu-__addonRef__-setMainNote" class="menuitem-iconic popup-type-single-note" label="&zotero.__addonRef__.itemmenu.setMainNote.label;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'setMainNote', content: {params: {itemID: ZoteroPane.getSelectedItems()[0].id}}})" style="list-style-image: url('chrome://__addonRef__/skin/favicon.png');" />
<menuitem id="zotero-itemmenu-__addonRef__-exportNote" class="menuitem-iconic popup-type-single" label="&zotero.__addonRef__.itemmenu.exportNote.label;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent.bind(Zotero.BetterNotes.ZoteroEvents)({type: 'exportNotes', content: {}})" style="list-style-image: url('chrome://__addonRef__/skin/favicon.png');" />
<menuitem id="zotero-itemmenu-__addonRef__-exportNotes" class="menuitem-iconic popup-type-multiple" label="&zotero.__addonRef__.itemmenu.exportNotes.label;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent.bind(Zotero.BetterNotes.ZoteroEvents)({type: 'exportNotes', content: {}})" style="list-style-image: url('chrome://__addonRef__/skin/favicon.png');" />
</popup>
<menupopup id="menu_FilePopup">
<menuseparator class="menu-type-betternotes menu-betternotes" />
<menuitem id="menu_new_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.new;" key="key_new_betternotes" accesskey="N" command="cmd_new_betternotes" />
<menuitem id="menu_open_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.open;" key="key_open_betternotes" accesskey="O" command="cmd_open_betternotes" />
<menuitem id="menu_export_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.export;" key="key_export_betternotes" accesskey="E" command="cmd_export_betternotes" />
<menuitem id="menu_sync_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.sync;" key="key_sync_betternotes" accesskey="S" command="cmd_sync_betternotes" />
<menuitem id="menu_sync_manager_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.syncmanager;" command="cmd_sync_manager_betternotes" />
<menuseparator class="menu-betternotes" />
</menupopup>
<menupopup id="menu_EditPopup">
<menu id="menu_insertTextTemplate_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.insertTextTemplate;">
<menupopup id="menu_insertTextTemplatePopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateTemplateMenu(arguments[0], 'Text');" />
</menu>
<menu id="menu_insertItemTemplate_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.insertItemTemplate;">
<menupopup id="menu_insertItemTemplatePopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateTemplateMenu(arguments[0], 'Item');" />
</menu>
<menuitem id="menu_editTemplate_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.editTemplate;" command="cmd_editTemplate_betternotes" />
<menu id="menu_citeSetting_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.citeSetting;">
<menupopup id="menu_citeSettingPopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateCitationStyleMenu();" />
</menu>
<!-- <menuitem id="menu_importlink_betternotes" class="menu-type-betternotes" label="&zotero.__addonRef__.workspace.menu.importLink;" command="cmd_importlink_betternotes" />
<menuitem id="menu_updatelink_betternotes" class="menu-type-betternotes" label="&zotero.__addonRef__.workspace.menu.updateLink;" command="cmd_updatelink_betternotes" /> -->
<menuseparator class="menu-betternotes" />
<menuitem id="menu_autoannotation_betternotes" class="menu-betternotes" type="checkbox" label="&zotero.__addonRef__.workspace.menu.autoannotation;" command="cmd_autoannotation_betternotes" />
<menuseparator class="menu-betternotes" />
<menuitem id="menu_convertmd_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.convertmd;" command="cmd_convertmd_betternotes" />
<menuitem id="menu_convertasciidoc_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.convertasciidoc;" command="cmd_convertasciidoc_betternotes" />
<menuseparator class="menu-betternotes" />
</menupopup>
<menupopup id="menu_viewPopup">
<menuitem id="menu_openWindow_betternotes" class="menu-type-betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.openWindow;" command="cmd_open_betternotesWindow" />
<menuseparator class="menu-type-betternotes menu-betternotes" />
<menuitem id="menu_treeview" class="menu-type-betternotes menu-betternotes" type="checkbox" label="&zotero.__addonRef__.workspace.menu.treeview;" command="cmd_treeview_betternotes" />
<menuitem id="menu_mindmap" class="menu-type-betternotes menu-betternotes" type="checkbox" label="&zotero.__addonRef__.workspace.menu.mindmap;" command="cmd_mindmap_betternotes" />
<menuitem id="menu_bubblemap" class="menu-type-betternotes menu-betternotes" type="checkbox" label="&zotero.__addonRef__.workspace.menu.bubblemap;" command="cmd_bubblemap_betternotes" />
<menuseparator class="menu-type-betternotes menu-betternotes" />
<menuitem id="menu_wordcount_betternotes" class="menu-type-betternotes menu-betternotes" disabled="true" />
</menupopup>
<menupopup id="menu_HelpPopup">
<menuseparator class="menu-betternotes" />
<menu id="menu_ocrsetting_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.OCRSetting;">
<menupopup id="menu_ocrsettingpopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateOCRStyleMenu();">
<menuitem id="menu_ocr_bing_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.OCRBing;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent.bind(Zotero.BetterNotes.ZoteroEvents)({type: 'setOCREngine', content: {params: {engine: 'bing'}}})" type="checkbox" />
<menuitem id="menu_ocr_mathpix_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.OCRMathpix;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent.bind(Zotero.BetterNotes.ZoteroEvents)({type: 'setOCREngine', content: {params: {engine: 'mathpix'}}})" type="checkbox" />
<menuitem id="menu_ocr_xunfei_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.OCRXunfei;" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent.bind(Zotero.BetterNotes.ZoteroEvents)({type: 'setOCREngine', content: {params: {engine: 'xunfei'}}})" type="checkbox" />
</menupopup>
</menu>
<menuitem id="menu_guide_betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.guide;" command="cmd_guide_betternotes" />
<menuitem id="menu_about_betternotes menu-betternotes" label="&zotero.__addonRef__.workspace.menu.about;" command="cmd_about_betternotes" />
</menupopup>
</overlay>

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
type="text/css"
href="chrome://__addonRef__/content/lib/css/github-markdown-light.min.css"
/>
<style>
@page {
margin: 25mm 20mm 25mm 20mm;
size: A4;
}
@media print {
body {
height: auto;
margin: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
@page: footer {
display: none;
}
@page: header {
display: none;
}
}
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 0px;
}
td,
th {
word-break: break-all;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
</style>
<script></script>
</head>
<body>
<article class="markdown-body">
<h1>Print to PDF</h1>
<p>Waiting for data...</p>
</article>
</body>
</html>

View File

@ -0,0 +1,69 @@
<vbox
id="zotero-prefpane-__addonRef__"
onload="Zotero.__addonInstance__.hooks.onPrefsEvent('load', {window})"
>
<groupbox>
<label><html:h2>&zotero.__addonRef__.pref.workspace.title;</html:h2></label>
<hbox>
<html:label for="__addonRef__-workspace-expandLevel"
>&zotero.__addonRef__.pref.workspace.expandLevel.label;</html:label
>
<html:input
type="number"
min="1"
max="6"
step="1"
id="__addonRef__-workspace-expandLevel"
preference="__prefsPrefix__.workspace.outline.expandLevel"
></html:input>
</hbox>
</groupbox>
<groupbox>
<label><html:h2>&zotero.__addonRef__.pref.sync.title;</html:h2></label>
<hbox>
<html:label for="__addonRef__-sync-period"
>&zotero.__addonRef__.pref.sync.period.label;</html:label
>
<html:input
type="number"
min="-1"
max="3600"
step="1"
placeholder="-1 for disable"
id="__addonRef__-sync-period"
preference="__prefsPrefix__.syncPeriodSeconds"
></html:input>
</hbox>
<hbox>
<button
onclick="Zotero.__addonInstance__.api.window.showSyncManager()"
label="&zotero.__addonRef__.pref.sync.manager.label;"
></button>
</hbox>
</groupbox>
<groupbox>
<label><html:h2>&zotero.__addonRef__.pref.template.title;</html:h2></label>
<hbox>
<button
onclick="Zotero.__addonInstance__.api.window.showTemplateEditor()"
label="&zotero.__addonRef__.pref.template.editor.label;"
></button>
</hbox>
</groupbox>
<groupbox>
<label
><html:h2>&zotero.__addonRef__.pref.annotation.title;</html:h2></label
>
<checkbox
id="__addonRef__-enableAddToNote"
label="&zotero.__addonRef__.pref.annotation.autoAnnotation.label;"
native="true"
preference="__prefsPrefix__.autoAnnotation"
/>
</groupbox>
</vbox>
<vbox>
<label
value="&zotero.__addonRef__.help.version.label; &zotero.__addonRef__.help.releasetime.label;"
></label>
</vbox>

View File

@ -1,27 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % ZoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%ZoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<dialog id="betternotes-sync-dialog" windowtype="betternotes-sync" title="&zotero.__addonRef__.sync.title;" orient="vertical" width="300" height="300" buttons="cancel,accept,help" ondialogaccept="Zotero.BetterNotes.SyncInfoWindow.doAccept();" ondialoghelp="Zotero.BetterNotes.SyncInfoWindow.doExport();" onload="Zotero.BetterNotes.SyncInfoWindow.doLoad(window);" onunload="Zotero.BetterNotes.SyncInfoWindow.doUnload();" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" style="padding:2em" persist="screenX screenY width height" buttonlabelhelp="&zotero.__addonRef__.sync.export.label;">
<script src="chrome://zotero/content/include.js" />
<vbox flex="1">
<label value="&zotero.__addonRef__.sync.path.label;" />
<label id="__addonRef__-sync-path" />
<label value="&zotero.__addonRef__.sync.lastsync.label;" />
<label id="__addonRef__-sync-lastsync" />
<hbox>
<checkbox id="__addonRef__-sync-enable" label="&zotero.__addonRef__.sync.enable.label;" checked="true" />
<button flex="0" label="&zotero.__addonRef__.sync.dosync.label;" oncommand="Zotero.BetterNotes.SyncController.doSync(null, true)"></button>
<button flex="0" label="&zotero.__addonRef__.sync.synclist.label;" oncommand="Zotero.BetterNotes.SyncListWindow.openSyncList()"></button>
</hbox>
</vbox>
</dialog>

View File

@ -8,7 +8,7 @@
type="text/css"
href="chrome://__addonRef__/content/lib/css/dx.light.css"
/>
<link rel="stylesheet" href="chrome://__addonRef__/skin/workspace.css" />
<link rel="stylesheet" href="chrome://__addonRef__/content/tooltip.css" />
<link
rel="stylesheet"
type="text/css"

View File

@ -1,52 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/standalone.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % standaloneDTD SYSTEM "chrome://zotero/locale/standalone.dtd">
%standaloneDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<window id="zotero-knowledge-sync-list" onload="Zotero.BetterNotes.SyncListWindow.doLoad(window);" onresize="Zotero.BetterNotes.SyncListWindow.doUpdate();" orient="vertical" width="600" height="350" title="&zotero.__addonRef__.syncList.title;" persist="screenX screenY width height" windowtype="zotero:knowledgeSyncList" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
<script src="chrome://zotero/content/include.js" />
<keyset>
<key id="key_close" key="W" modifiers="accel" command="cmd_close" />
<key id="key_selectall" key="A" modifiers="accel" command="cmd_selectall" />
<key id="key_selectall" keyCode="27" command="cmd_unselectall" />
</keyset>
<command id="cmd_close" oncommand="window.close();" />
<command id="cmd_selectall" oncommand="window.document.getElementById('sync-list').selectAll();" />
<command id="cmd_unselectall" oncommand="window.document.getElementById('sync-list').clearSelection();" />
<vbox flex="1">
<listbox id="sync-list" flex="1" seltype="multiple" onselect="Zotero.BetterNotes.SyncListWindow.onSelect();">
<listhead>
<listheader id="icon" label="#" flex="1" />
<listheader id="notename" label="&zotero.__addonRef__.syncList.notename.label;" flex="1" />
<listheader id="lastsync" label="&zotero.__addonRef__.syncList.lastsync.label;" flex="1" />
<listheader id="syncpath" label="&zotero.__addonRef__.syncList.syncpath.label;" flex="1" />
</listhead>
<listcols>
<listcol flex="1" />
</listcols>
</listbox>
<hbox flex="0">
<button id="doupdate" label="&zotero.__addonRef__.syncList.doupdate.label;" oncommand="Zotero.BetterNotes.SyncListWindow.doUpdate();"></button>
<button id="changesyncperiod" label="&zotero.__addonRef__.syncList.changesyncperiod.label;" oncommand="Zotero.BetterNotes.SyncListWindow.changeSyncPeriod();"></button>
<button id="dosync" label="&zotero.__addonRef__.syncList.dosync.label;" oncommand="Zotero.BetterNotes.SyncListWindow.doSync();"></button>
<button id="changesync" label="&zotero.__addonRef__.syncList.changesync.label;" oncommand="Zotero.BetterNotes.SyncListWindow.changeSync();"></button>
<button id="removesync" label="&zotero.__addonRef__.syncList.removesync.label;" oncommand="Zotero.BetterNotes.SyncListWindow.removeSync();"></button>
</hbox>
</vbox>
</window>

View File

@ -0,0 +1,120 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css" type="text/css"?>
<!DOCTYPE html>
<html
lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="__addonRef__-syncManager"
sizemode="normal"
scrolling="false"
persist="screenX screenY width height sizemode"
>
<head>
<title locale-target="innerHTML">syncManager.title</title>
<meta charset="utf-8" />
<script>
document.addEventListener("DOMContentLoaded", (ev) => {
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
Services.scriptloader.loadSubScript(
"chrome://zotero/content/include.js",
this
);
Services.scriptloader.loadSubScript(
"resource://zotero/require.js",
this
);
window.arguments[0]._initPromise.resolve();
});
</script>
<style>
html,
body,
.viewport {
padding: 0;
margin: 0;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
word-wrap: break-word;
background-color: #f0f0f0;
font-family: initial;
}
.viewport {
margin: 0 5px 0 5px;
}
.viewport-container {
padding: 0;
margin: 0;
height: calc(100% - 50px);
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.footer-container {
padding: 5px;
margin: 0;
height: 30px;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.list-viewport {
width: 100%;
background-color: #ffffff;
}
.tool-button {
width: 75px;
max-width: 75px;
min-width: 75px;
padding: 5px;
text-align: center;
margin: 0 10px 0 10px;
}
.virtualized-table-body {
overflow: hidden;
}
</style>
</head>
<body>
<div class="viewport-container">
<div id="list-container" class="viewport list-viewport">
<div id="table-container"></div>
</div>
</div>
<div
class="footer-container"
style="justify-content: flex-start; padding: 10px"
>
<button class="tool-button" id="refresh" locale-target="innerHTML">
syncManager.refresh
</button>
<button
class="tool-button"
id="sync"
locale-target="innerHTML,title"
title="syncManager.syncDetail"
>
syncManager.sync
</button>
<button
class="tool-button"
id="unSync"
locale-target="innerHTML,title"
title="syncManager.unSyncDetail"
>
syncManager.unSync
</button>
</div>
</body>
</html>

View File

@ -1,66 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/standalone.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % standaloneDTD SYSTEM "chrome://zotero/locale/standalone.dtd">
%standaloneDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<window id="zotero-knowledge-template-editor" onload="Zotero.BetterNotes.TemplateWindow.initTemplates(window);" orient="vertical" width="600" height="350" title="&zotero.__addonRef__.template.title;" persist="screenX screenY width height" windowtype="zotero:knowledgeTemplateEditor" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
<script src="chrome://zotero/content/include.js" />
<keyset>
<key id="key_save" key="S" modifiers="accel" command="cmd_save" />
<key id="key_help" key="H" modifiers="accel" command="cmd_help" />
<key id="key_close" key="W" modifiers="accel" command="cmd_close" />
</keyset>
<command id="cmd_save" oncommand="Zotero.BetterNotes.TemplateWindow.saveSelectedTemplate();" />
<command id="cmd_help" oncommand="Zotero.launchURL('https://github.com/windingwind/zotero-better-notes/blob/master/Template.md');" />
<command id="cmd_more" oncommand="Zotero.launchURL('https://github.com/windingwind/zotero-better-notes/discussions/categories/note-templates');" />
<command id="cmd_close" oncommand="window.close();" />
<hbox flex="1">
<vbox id="template-container" flex="1" minwidth="400px">
<listbox id="template-list" flex="1" seltype="single" onselect="Zotero.BetterNotes.TemplateWindow.updateEditorView();">
<listhead>
<listheader id="name" label="&zotero.__addonRef__.template.list.label;" flex="1" />
</listhead>
<listcols>
<listcol flex="1" />
</listcols>
</listbox>
<row style="margin: 10px 0 10px 0;">
<button id="create-template" label="&zotero.__addonRef__.template.create.label;" oncommand="Zotero.BetterNotes.TemplateWindow.createTemplate();"></button>
<button id="import-template" label="&zotero.__addonRef__.template.import.label;" oncommand="Zotero.BetterNotes.TemplateWindow.importNoteTemplate();"></button>
<button id="help" label="&helpMenu.label;" command="cmd_help" key="key_help" accesskey="H"></button>
<button id="more" label="&zotero.__addonRef__.template.more.label;" command="cmd_more"></button>
</row>
</vbox>
<splitter></splitter>
<rows minwidth="400px">
<row style="margin: 10px 0 10px 0;">
<label value="&zotero.__addonRef__.template.header.label;"></label>
<textbox id="editor-name" disabled="true"></textbox>
</row>
<row flex="1">
<textbox id="editor-textbox" flex="1" multiline="true" disabled="true"></textbox>
</row>
<row style="margin: 10px 0 10px 0;">
<button id="save-template" label="&zotero.__addonRef__.template.save.label;" command="cmd_save" key="key_save" accesskey="S"></button>
<button id="delete-template" label="&zotero.__addonRef__.template.delete.label;" oncommand="Zotero.BetterNotes.TemplateWindow.deleteSelectedTemplate();"></button>
<button id="reset-template" label="&zotero.__addonRef__.template.reset.label;" oncommand="Zotero.BetterNotes.TemplateWindow.resetSelectedTemplate();" hidden="hidden"></button>
</row>
</rows>
</hbox>
</window>

View File

@ -0,0 +1,201 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css" type="text/css"?>
<!DOCTYPE html>
<html
lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="__addonRef__-templateEditor"
sizemode="normal"
scrolling="false"
persist="screenX screenY width height sizemode"
>
<head>
<title locale-target="innerHTML">templateEditor.title</title>
<meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
<script>
document.addEventListener("DOMContentLoaded", (ev) => {
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
Services.scriptloader.loadSubScript(
"chrome://zotero/content/include.js",
this
);
Services.scriptloader.loadSubScript(
"resource://zotero/require.js",
this
);
window.arguments[0]._initPromise.resolve();
});
</script>
<style>
html,
body,
.viewport {
padding: 0;
margin: 0;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
word-wrap: break-word;
background-color: #f0f0f0;
font-family: initial;
}
.viewport {
margin: 0 5px 0 5px;
}
.viewport-container {
padding: 0;
margin: 0;
height: calc(100% - 50px);
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.footer-container {
padding: 5px;
margin: 0;
height: 30px;
display: flex;
flex-direction: row;
overflow: hidden;
background-color: #f0f0f0;
}
.list-viewport {
width: calc(20% - 10px);
background-color: #ffffff;
}
.editor-viewport {
display: flex;
flex-direction: column;
width: calc(40% - 10px);
padding: 5px;
}
.preview-viewport {
display: flex;
flex-direction: column;
width: calc(40% - 10px);
padding: 5px;
}
.tool-button {
width: 75px;
max-width: 75px;
min-width: 75px;
padding: 5px;
text-align: center;
margin: 0 10px 0 10px;
}
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 0px;
}
</style>
<link
rel="stylesheet"
type="text/css"
href="chrome://__addonRef__/content/lib/css/github-markdown-light.min.css"
/>
</head>
<body>
<div class="viewport-container">
<div id="list-container" class="viewport list-viewport">
<div id="table-container"></div>
</div>
<div class="viewport editor-viewport">
<div style="display: flex">
<div style="flex-shrink: 0" locale-target="innerHTML">
templateEditor.templateName
</div>
<input id="editor-name" type="text" style="width: 100%" />
</div>
<div style="display: flex; flex-direction: column; height: 100%">
<textarea
id="editor-textbox"
style="height: 100%; resize: none"
></textarea>
</div>
</div>
<div class="viewport preview-viewport">
<div style="display: flex">
<div style="flex-shrink: 0" locale-target="innerHTML">
templateEditor.previewContainer
</div>
</div>
<article id="preview-container" class="markdown-body"></article>
</div>
</div>
<div
class="footer-container"
style="justify-content: flex-start; padding: 10px"
>
<button
class="tool-button"
id="create"
locale-target="innerHTML,title"
title="templateEditor.createDetail"
>
templateEditor.create
</button>
<button
class="tool-button"
id="import"
locale-target="innerHTML,title"
title="templateEditor.importDetail"
>
templateEditor.import
</button>
<button
class="tool-button"
id="more"
locale-target="innerHTML,title"
title="templateEditor.moreDetail"
>
templateEditor.more
</button>
<button
class="tool-button"
id="save"
locale-target="innerHTML,title"
title="templateEditor.saveDetail"
>
templateEditor.save
</button>
<button
class="tool-button"
id="delete"
locale-target="innerHTML,title"
title="templateEditor.deleteDetail"
>
templateEditor.delete
</button>
<button
class="tool-button"
id="reset"
locale-target="innerHTML,title"
title="templateEditor.resetDetail"
>
templateEditor.reset
</button>
<button
class="tool-button"
id="help"
locale-target="innerHTML,title"
title="templateEditor.helpDetail"
>
templateEditor.help
</button>
</div>
</body>
</html>

View File

@ -84,7 +84,7 @@
items,
expandNodesRecursive: false,
dataStructure: "plain",
height: $("window").height() - 100,
height: $("window").height(),
displayExpr: "name",
noDataText: "Outline is Empty",
onItemClick: jumpNode,
@ -127,7 +127,7 @@
e.dropInsideItem &&
toNode !== null &&
// !toNode.itemData.isDirectory &&
toNode.itemData.rank === 7
toNode.itemData.level === 7
) {
return;
}
@ -160,7 +160,11 @@
}
function getTreeView() {
try {
return $("#treeview").dxTreeView("instance");
} catch {
return undefined;
}
}
function calculateToIndex(e) {
@ -267,22 +271,44 @@
window.parent.postMessage({ type: "getMindMapData" }, "*");
}
function setData(nodes) {
function walkNode(node, callback) {
if (!node) {
return;
}
callback(node);
node.children.forEach((child) => walkNode(child, callback));
}
function setData(nodes, expandLevel) {
console.log(nodes);
const tree = getTreeView();
const expandedStatus = {};
if (tree) {
const rootNodes = tree.getNodes();
console.log(tree, rootNodes);
rootNodes.forEach((root) => {
walkNode(root, (node) => {
expandedStatus[node.itemData.name] = node.itemData.expanded;
});
});
}
console.log(expandedStatus);
const treeData = [];
nodes.map((node) => {
treeData.push({
id: String(node.model.id),
name: node.model.name,
rank: node.model.rank,
icon: node.model.rank === 7 ? noteIcon : undefined,
level: node.model.level,
icon: node.model.level === 7 ? noteIcon : undefined,
lineIndex: node.model.lineIndex,
endIndex: node.model.endIndex,
isDirectory: node.children.length > 0,
expanded:
node.children.filter((n) => n.model.rank < 7).length !== 0,
node.model.name in expandedStatus
? expandedStatus[node.model.name]
: node.model.level < expandLevel,
parentId:
node.model.rank > 1 ? String(node.parent.model.id) : undefined,
node.model.level > 1 ? String(node.parent.model.id) : undefined,
});
});
$(() => {
@ -296,7 +322,7 @@
if (itemData.noteLink) {
window.parent.postMessage(
{
type: "jumpNote",
type: "openNote",
link: itemData.noteLink,
id: parseInt(itemData.id),
},
@ -308,6 +334,7 @@
type: "jumpNode",
lineIndex: itemData.lineIndex,
id: parseInt(itemData.id),
workspaceType: window.workspaceType || "tab",
},
"*"
);
@ -317,7 +344,8 @@
function handler(e) {
console.log(e);
if (e.data.type === "setMindMapData") {
setData(e.data.nodes);
setData(e.data.nodes, e.data.expandLevel);
window.workspaceType = e.data.workspaceType;
}
}

View File

@ -1,61 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<!DOCTYPE window [
<!ENTITY % ZoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%ZoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="knowledge-wizard" windowtype="knowledge-wizard" title="&zotero.__addonRef__.wizard.title;" width="788" height="600" onwizardfinish="Zotero.BetterNotes.WizardWindow.setup()">
<script src="chrome://zotero/content/include.js" />
<wizardpage label="&zotero.__addonRef__.wizard.title;">
<image src="chrome://__addonRef__/skin/knowledge-app.png" width="560" height="400"></image>
<description style="white-space: pre-wrap; font-size: large; font-weight: bold;">&zotero.__addonRef__.wizard.page1.header;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page1.description;</description>
</wizardpage>
<wizardpage label="&zotero.__addonRef__.wizard.open;">
<image src="chrome://__addonRef__/skin/open-knowledge.png" width="560" height="400"></image>
<description style="white-space: pre-wrap; font-size: large; font-weight: bold;">&zotero.__addonRef__.wizard.page3.header;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page3.description;</description>
</wizardpage>
<wizardpage label="&zotero.__addonRef__.wizard.layout;">
<image src="chrome://__addonRef__/skin/intro-workspace.png" width="560" height="400"></image>
<description style="white-space: pre-wrap; font-size: large; font-weight: bold;">&zotero.__addonRef__.wizard.page4.header;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page4.description1;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page4.description2;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page4.description3;</description>
</wizardpage>
<wizardpage label="&zotero.__addonRef__.wizard.link;">
<image src="chrome://__addonRef__/skin/add-link.gif" width="560" height="400" style="padding-top: 50px;"></image>
<description style="white-space: pre-wrap; font-size: large; font-weight: bold;">&zotero.__addonRef__.wizard.page5.header;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page5.description1;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page5.description2;</description>
</wizardpage>
<wizardpage label="&zotero.__addonRef__.wizard.setup;">
<description style="white-space: pre-wrap; font-size: large; font-weight: bold;">&zotero.__addonRef__.wizard.page6.header;</description>
<description style="white-space: pre-wrap;">&zotero.__addonRef__.wizard.page6.description1;</description>
<description style="white-space: pre-wrap; color: red;">&zotero.__addonRef__.wizard.page6.description2;</description>
<rows>
<row>
<checkbox id="__addonRef__-setup-enable" checked="true" oncommand="Zotero.BetterNotes.WizardWindow.changeSetup()" />
<label value="&zotero.__addonRef__.wizard.setup.enable.label;" />
</row>
<row>
<checkbox id="__addonRef__-setup-collectionenable" checked="true" oncommand="Zotero.BetterNotes.WizardWindow.updateCollectionSetup()" />
<label value="&zotero.__addonRef__.wizard.setup.collectionenable.label;" />
<textbox id="__addonRef__-setup-collectionname" value="My Notes" oninput="Zotero.BetterNotes.WizardWindow.updateCollectionSetup()" />
</row>
<row>
<checkbox id="__addonRef__-setup-noteenable" checked="true" oncommand="Zotero.BetterNotes.WizardWindow.updateNoteSetup()" />
<label value="&zotero.__addonRef__.wizard.setup.noteenable.label;" />
</row>
</rows>
</wizardpage>
<script>Zotero.BetterNotes.WizardWindow.init(document)</script>
</wizard>

View File

@ -1,178 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/standalone.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<?xml-stylesheet href="chrome://__addonRef__/skin/workspace.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % standaloneDTD SYSTEM "chrome://zotero/locale/standalone.dtd">
%standaloneDTD;
<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://zotero/locale/mozilla/editMenuOverlay.dtd">
%editMenuOverlayDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % knowledgeDTD SYSTEM "chrome://__addonRef__/locale/overlay.dtd">
%knowledgeDTD;
]>
<window id="zotero-knowledge-workspace" orient="vertical" width="1000" height="350" title="&zotero.__addonRef__.workspace.title;" persist="screenX screenY width height sizemode" windowtype="zotero:knowledgeWorkspace" onload="Zotero.BetterNotes.WorkspaceWindow._workspacePromise.resolve()" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
<script src="chrome://zotero/content/include.js" />
<keyset>
<key id="key_new" key="N" modifiers="accel" command="cmd_new" />
<key id="key_open" key="O" modifiers="accel" command="cmd_open" />
<key id="key_export" key="E" modifiers="accel" command="cmd_export" />
<key id="key_sync_betternotes" key="S" modifiers="accel,shift" command="cmd_sync_betternotes" />
<key id="key_close" key="W" modifiers="accel" command="cmd_close" />
<!-- <key id="key_insertNotes" key="I" modifiers="accel" command="cmd_insertNotes" /> -->
<key id="key_indent" keycode="VK_TAB" command="cmd_indent_betternotes" />
<key id="key_unindent" keycode="VK_TAB" modifiers="shift" command="cmd_unindent_betternotes" />
</keyset>
<command id="cmd_new" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'createWorkspace'});" />
<command id="cmd_open" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'selectMainNote'});" />
<command id="cmd_openWindow" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openWorkspaceInWindow'});" />
<command id="cmd_export" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'export', content: {editorInstance: {_item: false}}});" />
<command id="cmd_sync_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'sync'});" />
<command id="cmd_sync_manager_betternotes" oncommand="Zotero.BetterNotes.SyncListWindow.openSyncList();" />
<command id="cmd_close" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'closeWorkspace'});" />
<!-- <command id="cmd_insertNotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'insertNotes'});" /> -->
<command id="cmd_editTemplate" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'editTemplate'});" />
<!-- <command id="cmd_importlink_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'importLink'});" />
<command id="cmd_updatelink_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'updateLink'});" /> -->
<command id="cmd_autoannotation_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'updateAutoAnnotation', content: {event: event}});" />
<command id="cmd_convertmd_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'convertMD'});" />
<command id="cmd_convertasciidoc_betternotes" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'convertAsciiDoc'});" />
<command id="cmd_treeview" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(1);" />
<command id="cmd_mindmap" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(2);" />
<command id="cmd_bubblemap" oncommand="Zotero.BetterNotes.WorkspaceOutline.switchView(3);" />
<command id="cmd_guide" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openUserGuide'});" />
<command id="cmd_about" oncommand="Zotero.BetterNotes.ZoteroEvents.onEditorEvent({type: 'openAbout'});" />
<!-- Menu -->
<menubar id="better-notes-menu">
<menu id="fileMenu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
<menupopup id="menu_FilePopup">
<menuitem id="menu_new" label="&zotero.__addonRef__.workspace.menu.new;" key="key_new" accesskey="N" command="cmd_new" />
<menuitem id="menu_open" label="&zotero.__addonRef__.workspace.menu.open;" key="key_open" accesskey="O" command="cmd_open" />
<menuitem id="menu_export" label="&zotero.__addonRef__.workspace.menu.export;" key="key_export" accesskey="E" command="cmd_export" />
<menuitem id="menu_sync_betternotes" label="&zotero.__addonRef__.workspace.menu.sync;" key="key_sync_betternotes" accesskey="S" command="cmd_sync_betternotes" />
<menuitem id="menu_sync_manager_betternotes" label="&zotero.__addonRef__.workspace.menu.syncmanager;" command="cmd_sync_manager_betternotes" />
<menuitem id="menu_close" label="&closeCmd.label;" key="key_close" accesskey="&closeCmd.accesskey;" command="cmd_close" />
</menupopup>
</menu>
<menu id="menu_edit" label="&editMenu.label;" accesskey="&editMenu.accesskey;">
<menupopup id="menu_EditPopup">
<!-- <menuitem id="menu_insertNotes" label="&zotero.__addonRef__.workspace.menu.insertNotes;" key="key_insertNotes" accesskey="I" command="cmd_insertNotes" /> -->
<menu id="menu_insertTextTemplate" label="&zotero.__addonRef__.workspace.menu.insertTextTemplate;">
<menupopup id="menu_insertTextTemplatePopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateTemplateMenu(arguments[0], 'Text');" />
</menu>
<menu id="menu_insertItemTemplate" label="&zotero.__addonRef__.workspace.menu.insertItemTemplate;">
<menupopup id="menu_insertItemTemplatePopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateTemplateMenu(arguments[0], 'Item');" />
</menu>
<menuitem id="menu_editTemplate" label="&zotero.__addonRef__.workspace.menu.editTemplate;" command="cmd_editTemplate" />
<menu id="menu_citeSetting_betternotes" class="menu-type-betternotes" label="&zotero.__addonRef__.workspace.menu.citeSetting;">
<menupopup id="menu_citeSettingPopup" onpopupshowing="Zotero.BetterNotes.ZoteroViews.updateCitationStyleMenu();" />
</menu>
<!-- <menuitem id="menu_importlink_betternotes" class="menu-type-betternotes" label="&zotero.__addonRef__.workspace.menu.importLink;" command="cmd_importlink_betternotes" />
<menuitem id="menu_updatelink_betternotes" class="menu-type-betternotes" label="&zotero.__addonRef__.workspace.menu.updateLink;" command="cmd_updatelink_betternotes" /> -->
<menuseparator />
<menuitem id="menu_autoannotation_betternotes" type="checkbox" label="&zotero.__addonRef__.workspace.menu.autoannotation;" command="cmd_autoannotation_betternotes" />
<menuseparator />
<menuitem id="menu_convertmd_betternotes" label="&zotero.__addonRef__.workspace.menu.convertmd;" command="cmd_convertmd_betternotes" />
<menuitem id="menu_convertasciidoc_betternotes" class="menu-betternotes" label="&zotero.__addonRef__.workspace.menu.convertasciidoc;" command="cmd_convertasciidoc_betternotes" />
</menupopup>
</menu>
<menu id="view-menu" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;" onpopupshowing="Zotero.BetterNotes.WorkspaceMenu.updateViewMenu();">
<menupopup id="menu_ViewPopup">
<menuitem id="menu_openWindow" label="&zotero.__addonRef__.workspace.menu.openWindow;" command="cmd_openWindow" />
<menuseparator />
<menuitem id="menu_treeview" type="checkbox" label="&zotero.__addonRef__.workspace.menu.treeview;" command="cmd_treeview" />
<menuitem id="menu_mindmap" type="checkbox" label="&zotero.__addonRef__.workspace.menu.mindmap;" command="cmd_mindmap" />
<menuitem id="menu_bubblemap" type="checkbox" label="&zotero.__addonRef__.workspace.menu.bubblemap;" command="cmd_bubblemap" />
<menuseparator />
<menu id="note-font-size-menu" label="&noteFontSize.label;">
<menupopup oncommand="Zotero.Prefs.set('note.fontSize', event.originalTarget.getAttribute('label'));">
<menuitem label="11" type="checkbox" />
<menuitem label="12" type="checkbox" />
<menuitem label="13" type="checkbox" />
<menuitem label="14" type="checkbox" />
<menuitem label="18" type="checkbox" />
<menuitem label="24" type="checkbox" />
<menuitem label="36" type="checkbox" />
<menuitem label="48" type="checkbox" />
<menuitem label="64" type="checkbox" />
<menuitem label="72" type="checkbox" />
<menuitem label="96" type="checkbox" />
<menuseparator />
<menuitem id="view-menuitem-note-font-size-reset" label="&zotero.general.reset;" oncommand="Zotero.Prefs.clear('note.fontSize');" />
</menupopup>
</menu>
<menuseparator />
<menuitem id="menu_wordcount_betternotes" disabled="true" />
</menupopup>
</menu>
<menu id="helpMenu" label="&helpMenu.label;" accesskey="&helpMenu.accesskey;">
<menupopup id="menu_HelpPopup">
<menuitem id="menu_guide" label="&zotero.__addonRef__.workspace.menu.guide;" command="cmd_guide" />
<menuitem id="menu_about" label="&zotero.__addonRef__.workspace.menu.about;" command="cmd_about" />
</menupopup>
</menu>
</menubar>
<hbox flex="1">
<vbox id="zotero-knowledge-outline" flex="1" width="330" minwidth="300" style="overflow: hidden;">
<html:div id="mindmap-container">
<!-- Inser Here -->
</html:div>
<html:div id="outline-tools" height="50" maxheight="50" minheight="50" style="display: flex; flex-flex-direction: row; justify-content: space-between; margin: 0px 20px 0px 20px;">
<div class="tooltip">
<html:button id="outline-switchview" class="tool-button">
<svg t="1652006549395" class="icon tool-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14217" width="18" height="18">
<path d="M928 637c-8.8 0-16-7.2-16-16v-77c0-53-43-96-96-96H560c-8.8 0-16-7.2-16-16v-32c0-8.8 7.2-16 16-16h80c53 0 96-43 96-96V144c0-53-43-96-96-96H384c-53 0-96 43-96 96v144c0 53 43 96 96 96h80c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H208c-53 0-96 43-96 96v77c0 8.8-7.2 16-16 16-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V733c0-53-43-96-96-96-8.8 0-16-7.2-16-16v-77c0-17.7 14.3-32 32-32h256c8.8 0 16 7.2 16 16v94c0 8.3-6.7 15-15 15h-1c-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V733c0-53-43-96-96-96h-1c-8.3 0-15-6.7-15-15v-94c0-8.8 7.2-16 16-16h256c17.7 0 32 14.3 32 32v77c0 8.8-7.2 16-16 16-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V733c0-53-43-96-96-96z m-736 64c17.6 0 32 14.4 32 32v131c0 17.6-14.4 32-32 32H96c-17.6 0-32-14.4-32-32V733c0-17.6 14.4-32 32-32h96z m368 0c17.6 0 32 14.4 32 32v131c0 17.6-14.4 32-32 32h-96c-17.6 0-32-14.4-32-32V733c0-17.6 14.4-32 32-32h96zM384 320c-17.6 0-32-14.4-32-32V144c0-17.6 14.4-32 32-32h256c17.6 0 32 14.4 32 32v144c0 17.6-14.4 32-32 32H384z m576 544c0 17.6-14.4 32-32 32h-96c-17.6 0-32-14.4-32-32V733c0-17.6 14.4-32 32-32h96c17.6 0 32 14.4 32 32v131z" p-id="14218"></path>
</svg>
</html:button>
<span class="tooltiptext">Switch Outline Mode</span>
</div>
<div>
<div class="tooltip">
<html:button id="outline-saveImage" class="tool-button">
<svg t="1668585252314" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6557" width="18" height="18">
<path d="M828.1 896.7H199.9c-73.2 0-132.8-59.6-132.8-132.8V514.5c0-17.7 14.3-32 32-32s32 14.3 32 32v249.4c0 37.9 30.9 68.8 68.8 68.8h628.2c37.9 0 68.8-30.9 68.8-68.8V514.5c0-17.7 14.3-32 32-32s32 14.3 32 32v249.4c0 73.2-59.6 132.8-132.8 132.8z" p-id="6558"></path>
<path d="M512.4 740.7c-17.7 0-32-14.3-32-32V151.8c0-17.7 14.3-32 32-32s32 14.3 32 32v556.9c0 17.7-14.3 32-32 32z" p-id="6559"></path>
<path d="M518.5 737.4c-8.2 0-16.4-3.1-22.6-9.4-12.5-12.5-12.5-32.8 0-45.3l156.9-156.9c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L541.1 728c-6.2 6.3-14.4 9.4-22.6 9.4z" p-id="6560"></path>
<path d="M506.2 737.2c-8.2 0-16.4-3.1-22.6-9.4L326.7 570.9c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l156.9 156.9c12.5 12.5 12.5 32.8 0 45.3-6.3 6.3-14.5 9.4-22.7 9.4z" p-id="6561"></path>
</svg>
</html:button>
<span class="tooltiptext">Save As Image</span>
</div>
<div class="tooltip">
<html:button id="outline-saveFreeMind" class="tool-button">
<svg t="1668587671777" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1778" width="18" height="18">
<path d="M903.526 542.788c-11.476-109.267-69.113-209.266-157.91-273.966 1.775-42.203-8.091-85.392-30.916-124.607C651.078 34.909 510.893-2.126 401.587 61.495c-75.837 44.141-116.869 125.136-113.649 207.104-69.606 50.492-123.007 124.277-147.063 213.792a391.809 391.809 0 0 0-11.196 59.809c-37.458 19.563-69.947 49.713-92.506 89.106-62.851 109.751-24.832 249.672 84.919 312.523 76.13 43.597 166.769 38.645 236.139-5.102a391.907 391.907 0 0 0 57.362 20.29c89.516 24.056 180.13 14.833 258.713-20.088 35.68 22.675 78.051 35.746 123.464 35.587 126.472-0.444 228.638-103.331 228.194-229.803-0.31-87.709-49.894-163.714-122.438-201.925zM343.497 234.956c7.264-51.226 37.068-98.744 85.254-126.791 83.531-48.619 190.659-20.317 239.278 63.214 11.79 20.256 19.044 41.901 22.096 63.762a175.057 175.057 0 0 1-2.102 60.33c-9.855 46.729-38.683 89.27-83.209 115.186-83.531 48.619-190.659 20.317-239.278-63.214-9.688-16.645-16.312-34.228-20.063-52.097a174.622 174.622 0 0 1-1.976-60.39z m9.101 640.606a174.59 174.59 0 0 1-51.354 31.888c-47.97 19.273-103.973 17.204-152.318-10.482-83.87-48.03-112.925-154.957-64.894-238.828 11.641-20.328 26.749-37.424 44.142-50.994a175.094 175.094 0 0 1 53.336-28.371c45.394-14.826 96.646-11.13 141.349 14.471 83.871 48.031 112.925 154.957 64.894 238.828-9.586 16.742-21.526 31.292-35.155 43.488z m77.009 31.305a339.803 339.803 0 0 1-24.38-7.545 229.776 229.776 0 0 0 29.388-40.412c62.851-109.751 24.832-249.672-84.919-312.523-51.188-29.314-108.937-36.677-162.371-25.063a339.779 339.779 0 0 1 5.7-24.919c18.21-67.764 55.959-125.048 105.538-167.454a229.77 229.77 0 0 0 20.304 45.656c63.621 109.306 203.806 146.341 313.112 82.72 51.001-29.685 86.26-76.04 102.911-128.148 57.693 49.633 96.988 117.799 111.025 192.597a229.761 229.761 0 0 0-49.756-5.258c-126.472 0.444-228.638 103.331-228.194 229.803 0.207 58.987 22.705 112.68 59.48 153.148-61.556 21.657-130.06 25.612-197.838 7.398z m367.972 13.649c-23.467 0.082-45.866-4.47-66.346-12.777a175.093 175.093 0 0 1-51.198-32.023c-35.495-31.897-57.89-78.104-58.07-129.584-0.34-96.649 77.735-175.274 174.384-175.614 19.296-0.068 37.87 2.998 55.249 8.705a174.572 174.572 0 0 1 53.254 28.504c40.693 31.906 66.915 81.451 67.111 137.175 0.34 96.649-77.735 175.274-174.384 175.614z" p-id="1779"></path>
</svg>
</html:button>
<span class="tooltiptext">Save As FreeMind</span>
</div>
</div>
</html:div>
</vbox>
<splitter id="outline-splitter" collapse="before">
<grippy></grippy>
</splitter>
<zoteronoteeditor id="zotero-note-editor-main" flex="1" width="700" onerror="return;onError()" />
<splitter id="preview-splitter" collapse="after" state="collapsed">
<grippy></grippy>
</splitter>
<zoteronoteeditor id="zotero-note-editor-preview" flex="1" width="500" onerror="return;onError()" />
</hbox>
</window>

View File

@ -1,22 +1,88 @@
library.workspace=Workspace
library.openWorkspace=Open Workspace
library.newMainNote=New Main Note
library.newMainNote.confirmHead=Will create a new note under collection
library.newMainNote.confirmTail=and set it the main note. Continue?
library.importMD=Import MarkDown as Note
editor.mainNote=Main Note
editor.recentMainNotes=Set Main Note from Recent
editor.refreshEditor=Refresh Editor
editor.previewInWorkspace=Preview in Workspace
editor.copyNoteLink=Copy Note Link
editor.copyNoteLinkOfLine=Copy Note Link of Current Line
editor.importMD=Import from MarkDown
editor.resizeImage=Resize Image
editor.resizeImage.prompt=Enter new width (px):
editor.previewImage=Preview Image
editor.insertAnchor=Insert Position Preview
editor.endOfNote=End of Note
menu.insertTextTemplate=Insert Note Template [Text]
menu.insertItemTemplate=Insert Note Template [Item]
export.withLinkedNotes.confirm=Export linked notes to markdown files?
export.withYAML.confirm=With YAML Front Matter?
pref.title=BNotes
menuItem.setMainNote=As Workspace Note
menuItem.exportNote=Export Note
menuEdit.templatePicker=Insert Template to Workspace Note
menuEdit.templateEditor=Template Editor
menuTools.syncManager=Sync Manager
templateEditor.title=Template Editor
templateEditor.templateName=Template Name
templateEditor.previewContainer=Preview
templateEditor.create=New
templateEditor.import=Import Note
templateEditor.more=More Templates
templateEditor.save=Save
templateEditor.delete=Delete
templateEditor.reset=Reset
templateEditor.help=Help
tab.name=Note Workspace
export.title=Export Notes
export.options.linkMode=Linked Notes Mode
export.options.MD=MarkDown(.md)
export.options.Docx=MS Word(.docx)
export.options.PDF=PDF(.pdf)
export.options.mm=Mind Map
export.options.note=Zotero Note
export.embedLink=All Embeded in One Export
export.standaloneLink=Each Converted to Standalone Exports
export.keepLink=Keep Zotero Links(zotero://note/)
export.exportMD=Export MD File(s)
export.setAutoSync=Set Auto-Sync
export.withYAMLHeader=With YAML Header
export.exportDocx=Export Docx File
export.exportPDF=Export PDF File
export.exportFreeMind=Export FreeMind File
export.exportNote=Export to New Zotero Note Item
export.confirm=Export
export.cancel=Close
export.target=Target
syncManager.title=Sync Manager
syncManager.refresh=Refresh
syncManager.sync=Sync
syncManager.syncDetail=Check and sync immedietly
syncManager.unSync=UnSync
syncManager.unSyncDetail=Remove selected notes from syncing list
syncManager.noteName=Note Name
syncManager.lastSync=Last Sync
syncManager.noteStatus=MarkDown Path
syncInfo.syncTo=MarkDown Path
syncInfo.lastSync=Last Sync
syncInfo.sync=Sync
syncInfo.unSync=UnSync
syncInfo.reveal=Show in Folder
syncInfo.manager=Sync Manager
syncInfo.export=Export As...
syncInfo.cancel=Close
fileInterface.sync=Sync to
sync.start.hint=Note Auto-Sync is enabled every
sync.stop.hint=Note Auto-Sync is disabled
sync.running.hint.title=Note Syncing
sync.running.hint.check=Check Status
sync.running.hint.updateMD=Update MarkDown
sync.running.hint.updateNote=Update Note
sync.running.hint.diff=Confirm Merge
sync.running.hint.finish=Finish
sync.running.hint.synced=Synced
sync.running.hint.upToDate=Up To Date
workspace.notesPane.hint=PDF NotePane is not accesible if no PDF files are opened.
workspace.switchOutline=Swith Outline Mode
workspace.saveOutlineImage=Save Image
workspace.saveOutlineFreeMind=Save MindMap
editor.toolbar.main=Workspace Note
editor.toolbar.settings.title=Workspace Settings
editor.toolbar.settings.openWorkspace=Open Note Workspace
editor.toolbar.settings.setWorkspace=Set as Workspace Note
editor.toolbar.settings.insertTemplate=Insert Template to Cursor Line
editor.toolbar.settings.copyLink=Copy Note Link
editor.toolbar.export.title=Export Note...

View File

@ -1,112 +1,12 @@
<!ENTITY zotero.__addonRef__.workspace.title "Workspace">
<!ENTITY zotero.__addonRef__.pref.workspace.title "Workspace">
<!ENTITY zotero.__addonRef__.pref.workspace.expandLevel.label "Outline expand to heading level">
<!ENTITY zotero.__addonRef__.pref.sync.title "Sync">
<!ENTITY zotero.__addonRef__.pref.sync.period.label "Auto-sync period (seconds)">
<!ENTITY zotero.__addonRef__.pref.sync.manager.label "Open Sync Manager">
<!ENTITY zotero.__addonRef__.pref.template.title "Template">
<!ENTITY zotero.__addonRef__.pref.template.editor.label "Open Template Editor">
<!ENTITY zotero.__addonRef__.pref.annotation.title "PDF Annotation">
<!ENTITY zotero.__addonRef__.pref.annotation.autoAnnotation.label "Automatically add new annotations to workspace note">
<!ENTITY zotero.__addonRef__.workspace.menu.new "Create New Main Note">
<!ENTITY zotero.__addonRef__.workspace.menu.open "Open Main Note...">
<!ENTITY zotero.__addonRef__.workspace.menu.openWindow "Open Workspace in Standalone Window">
<!ENTITY zotero.__addonRef__.workspace.menu.export "Export Main Note...">
<!ENTITY zotero.__addonRef__.workspace.menu.sync "Sync Main Note">
<!ENTITY zotero.__addonRef__.workspace.menu.syncmanager "Better Notes Sync Manager">
<!ENTITY zotero.__addonRef__.workspace.menu.insertNotes "Insert Notes...">
<!ENTITY zotero.__addonRef__.workspace.menu.insertTextTemplate "Insert Note Template [Text]">
<!ENTITY zotero.__addonRef__.workspace.menu.insertItemTemplate "Insert Note Template [Item]">
<!ENTITY zotero.__addonRef__.workspace.menu.editTemplate "Note Template Editor">
<!ENTITY zotero.__addonRef__.workspace.menu.citeSetting "Citation Style in Notes">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRSetting "Annotation Math OCR">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRBing "Bing">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRMathpix "Mathpix">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRXunfei "Xunfei">
<!ENTITY zotero.__addonRef__.workspace.menu.addheading "Insert Heading...">
<!ENTITY zotero.__addonRef__.workspace.menu.indent "Decrease Heading Level">
<!ENTITY zotero.__addonRef__.workspace.menu.unindent "Increase Heading Level">
<!ENTITY zotero.__addonRef__.workspace.menu.importLink "Import Selected Note Link Content">
<!ENTITY zotero.__addonRef__.workspace.menu.updateLink "Update Selected Note Link Text">
<!ENTITY zotero.__addonRef__.workspace.menu.autoannotation "Auto Insert New Annotations to Note">
<!ENTITY zotero.__addonRef__.workspace.menu.convertmd "Convert MarkDown in Clipboard">
<!ENTITY zotero.__addonRef__.workspace.menu.convertasciidoc "Convert AsciiDoc in Clipboard">
<!ENTITY zotero.__addonRef__.workspace.menu.treeview "Outline: Tree View">
<!ENTITY zotero.__addonRef__.workspace.menu.mindmap "Outline: Mind Map">
<!ENTITY zotero.__addonRef__.workspace.menu.bubblemap "Outline: Bubble Map">
<!ENTITY zotero.__addonRef__.workspace.menu.guide "Better Notes User Guide">
<!ENTITY zotero.__addonRef__.workspace.menu.about "About Better Notes">
<!ENTITY zotero.__addonRef__.itemmenu.setMainNote.label "Set Main Note">
<!ENTITY zotero.__addonRef__.itemmenu.exportNote.label "Export Note and SubNotes">
<!ENTITY zotero.__addonRef__.itemmenu.exportNotes.label "Export Notes and SubNotes to Markdown">
<!ENTITY zotero.__addonRef__.export.title "Export">
<!ENTITY zotero.__addonRef__.export.cannotworkwith.label "Cannot work with ">
<!ENTITY zotero.__addonRef__.export.workwith.label "Only work with ">
<!ENTITY zotero.__addonRef__.export.option.label "General">
<!ENTITY zotero.__addonRef__.export.link.enable.label "Embed Linked Notes">
<!ENTITY zotero.__addonRef__.export.note.enable.label "Export to new note">
<!ENTITY zotero.__addonRef__.export.markdown.label "MarkDown">
<!ENTITY zotero.__addonRef__.export.file.enable.label "Export to MarkDown File">
<!ENTITY zotero.__addonRef__.export.singlefile.enable.label "Export Linked Notes to MarkDown File">
<!ENTITY zotero.__addonRef__.export.enableautosync.enable.label "Auto Sync to Export Path">
<!ENTITY zotero.__addonRef__.export.yaml.enable.label "YAML Front Matter">
<!ENTITY zotero.__addonRef__.export.document.label "Document">
<!ENTITY zotero.__addonRef__.export.docx.enable.label "Export to MS Word Document">
<!ENTITY zotero.__addonRef__.export.pdf.enable.label "Export to PDF Document">
<!ENTITY zotero.__addonRef__.export.mindmap.label "Mind Map">
<!ENTITY zotero.__addonRef__.export.freemind.enable.label "FreeMind">
<!ENTITY zotero.__addonRef__.sync.title "Sync Status">
<!ENTITY zotero.__addonRef__.sync.export.label "Export to...">
<!ENTITY zotero.__addonRef__.sync.path.label "Sync to:">
<!ENTITY zotero.__addonRef__.sync.changesync.label "Change Folder">
<!ENTITY zotero.__addonRef__.sync.lastsync.label "Last sync:">
<!ENTITY zotero.__addonRef__.sync.enable.label "Keep in sync">
<!ENTITY zotero.__addonRef__.sync.dosync.label "Sync Now">
<!ENTITY zotero.__addonRef__.sync.synclist.label "Sync Manager">
<!ENTITY zotero.__addonRef__.syncList.title "Sync Manager">
<!ENTITY zotero.__addonRef__.syncList.notename.label "Name">
<!ENTITY zotero.__addonRef__.syncList.syncpath.label "Sync To">
<!ENTITY zotero.__addonRef__.syncList.lastsync.label "Last Sync">
<!ENTITY zotero.__addonRef__.syncList.doupdate.label "Refresh">
<!ENTITY zotero.__addonRef__.syncList.changesyncperiod.label "Sync Period:">
<!ENTITY zotero.__addonRef__.syncList.dosync.label "Sync">
<!ENTITY zotero.__addonRef__.syncList.changesync.label "Change Folder">
<!ENTITY zotero.__addonRef__.syncList.removesync.label "Unsync">
<!ENTITY zotero.__addonRef__.syncList.related.label "Apply to Related Notes">
<!ENTITY zotero.__addonRef__.wizard.title "Welcomed to Zotero Better Notes">
<!ENTITY zotero.__addonRef__.wizard.page1.header "Zotero Better Notes User Guide">
<!ENTITY zotero.__addonRef__.wizard.page1.description "Click Next to Continue.">
<!ENTITY zotero.__addonRef__.wizard.open "Knowledge Workspace">
<!ENTITY zotero.__addonRef__.wizard.page3.header "Knowledge Workspace">
<!ENTITY zotero.__addonRef__.wizard.page3.description "Click the link above 'My Library' or the home button in the note editor to open or focus the workspace.">
<!ENTITY zotero.__addonRef__.wizard.layout "Knowledge Workspace Layout">
<!ENTITY zotero.__addonRef__.wizard.page4.header "Outline/Main Note/Linked Note Preview">
<!ENTITY zotero.__addonRef__.wizard.page4.description1 "Modify/jump to headings inside main Note with the Outline;">
<!ENTITY zotero.__addonRef__.wizard.page4.description2 "Add links to the main note while reading. Export to MarkDown/HTML with full text supported;">
<!ENTITY zotero.__addonRef__.wizard.page4.description3 "Open and edit linked notes in the preview.">
<!ENTITY zotero.__addonRef__.wizard.link "Add Note Link">
<!ENTITY zotero.__addonRef__.wizard.page5.header "Note as you go, and open the links as your will.">
<!ENTITY zotero.__addonRef__.wizard.page5.description1 "Click to insert the current note link after the current line in main note.">
<!ENTITY zotero.__addonRef__.wizard.page5.description2 "Jump to notes even from other Apps via a note link!">
<!ENTITY zotero.__addonRef__.wizard.setup "Setup Workspace">
<!ENTITY zotero.__addonRef__.wizard.page6.header "Almost there. Please follow the user guide before starting.">
<!ENTITY zotero.__addonRef__.wizard.page6.description1 "You can come back later from the knowledge workspace.">
<!ENTITY zotero.__addonRef__.wizard.page6.description2 "WARNING If main Note is not set, this addon will not work properly.">
<!ENTITY zotero.__addonRef__.wizard.setup.enable.label "Enable Auto Setup">
<!ENTITY zotero.__addonRef__.wizard.setup.collectionenable.label "Create a new collection for main notes:">
<!ENTITY zotero.__addonRef__.wizard.setup.noteenable.label "Create a 'Zotero Better Notes User Guide: Workflow' note">
<!ENTITY zotero.__addonRef__.template.title "Template Editor">
<!ENTITY zotero.__addonRef__.template.header.label "Template Name">
<!ENTITY zotero.__addonRef__.template.list.label "Template List">
<!ENTITY zotero.__addonRef__.template.create.label "Create">
<!ENTITY zotero.__addonRef__.template.import.label "Create from Note">
<!ENTITY zotero.__addonRef__.template.save.label "Save">
<!ENTITY zotero.__addonRef__.template.delete.label "Delete">
<!ENTITY zotero.__addonRef__.template.reset.label "Reset">
<!ENTITY zotero.__addonRef__.template.more.label "Get More">
<!ENTITY zotero.__addonRef__.help.feedback.caption.label "User Guide and Feedback">
<!ENTITY zotero.__addonRef__.help.feedback.label "GitHub">
<!ENTITY zotero.__addonRef__.help.doc.caption.label "Documentations">
<!ENTITY zotero.__addonRef__.help.doc.label "Better Notes Handbook(Chinese, provide translation)">
<!ENTITY zotero.__addonRef__.help.version.label "VERSION __buildVersion__">
<!ENTITY zotero.__addonRef__.help.version.label "__addonName__ VERSION __buildVersion__">
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">

View File

@ -1,22 +1,88 @@
library.workspace=工作区
library.openWorkspace=打开工作区
library.newMainNote=新建笔记并设为主笔记
library.newMainNote.confirmHead=将在当前分类
library.newMainNote.confirmTail=创建新笔记并将它设为主笔记。确认继续吗?
library.importMD=导入Markdown为笔记
editor.mainNote=主笔记
editor.recentMainNotes=从最近笔记设置主笔记
editor.refreshEditor=刷新编辑器
editor.previewInWorkspace=在工作区中预览
editor.copyNoteLink=复制笔记链接
editor.copyNoteLinkOfLine=复制当前行的笔记链接
editor.importMD=从Markdown导入
editor.resizeImage=调整图片大小
editor.resizeImage.prompt=输入新的宽度(单位:像素):
editor.previewImage=预览图片
editor.insertAnchor=插入位置预览
editor.endOfNote=笔记结尾
menu.insertTextTemplate=插入笔记模板[文本/Text]
menu.insertItemTemplate=插入笔记模板[条目/Item]
export.withLinkedNotes.confirm=导出链接的子笔记到Markdown
export.withYAML.confirm=导出带有YAML Front Matter
pref.title=BNotes
menuItem.setMainNote=设为工作区主笔记
menuItem.exportNote=导出笔记
menuEdit.templatePicker=插入模板到工作区笔记
menuEdit.templateEditor=模板编辑器
menuTools.syncManager=同步管理器
templateEditor.title=模板编辑器
templateEditor.templateName=模板名称
templateEditor.previewContainer=预览
templateEditor.create=新建
templateEditor.import=导入笔记
templateEditor.more=更多模板
templateEditor.save=保存
templateEditor.delete=删除
templateEditor.reset=重置
templateEditor.help=帮助
tab.name=笔记工作区
export.title=导出笔记
export.options.linkMode=链接笔记模式
export.options.MD=MarkDown(.md)
export.options.Docx=MS Word(.docx)
export.options.PDF=PDF(.pdf)
export.options.mm=思维导图
export.options.note=Zotero笔记
export.embedLink=全部嵌入为一个导出
export.standaloneLink=分别单独导出
export.keepLink=保留Zotero链接(zotero://note/)
export.exportMD=导出MD文件
export.setAutoSync=设置自动同步
export.withYAMLHeader=带有YAML头
export.exportDocx=导出Word文件
export.exportPDF=导出PDF文件
export.exportFreeMind=导出FreeMind文件
export.exportNote=导出为Zotero笔记条目
export.confirm=导出
export.cancel=关闭
export.target=目标
syncManager.title=同步管理
syncManager.refresh=刷新
syncManager.sync=同步
syncManager.syncDetail=检查并立即同步
syncManager.unSync=取消同步
syncManager.unSyncDetail=从同步列表中移除选中的笔记
syncManager.noteName=笔记名称
syncManager.lastSync=最近同步
syncManager.filePath=MarkDown路径
syncInfo.syncTo=MarkDown路径
syncInfo.lastSync=最近同步
syncInfo.sync=同步
syncInfo.unSync=取消同步
syncInfo.reveal=在文件夹中显示
syncInfo.manager=同步管理
syncInfo.export=导出为...
syncInfo.cancel=关闭
sync.start.hint=自动同步已启用, 间隔
sync.stop.hint=自动同步已停止
sync.running.hint.title=笔记同步
sync.running.hint.check=检查状态
sync.running.hint.updateMD=更新MarkDown
sync.running.hint.updateNote=更新笔记
sync.running.hint.diff=确认合并
sync.running.hint.finish=同步完成
sync.running.hint.synced=已同步
sync.running.hint.upToDate=已最新
fileInterface.sync=同步到
workspace.notesPane.hint=PDF笔记侧栏在没有PDF文件打开时不可访问.
workspace.switchOutline=切换大纲模式
workspace.saveOutlineImage=保存图片
workspace.saveOutlineFreeMind=保存思维导图
editor.toolbar.main=工作区笔记
editor.toolbar.settings.title=工作区选项
editor.toolbar.settings.openWorkspace=打开笔记工作区
editor.toolbar.settings.setWorkspace=设为工作区笔记
editor.toolbar.settings.insertTemplate=插入模板到光标行
editor.toolbar.settings.copyLink=复制笔记链接
editor.toolbar.export.title=导出笔记...

View File

@ -1,112 +1,12 @@
<!ENTITY zotero.__addonRef__.workspace.title "工作区">
<!ENTITY zotero.__addonRef__.pref.workspace.title "工作区">
<!ENTITY zotero.__addonRef__.pref.workspace.expandLevel.label "大纲展开至标题层级">
<!ENTITY zotero.__addonRef__.pref.sync.title "同步">
<!ENTITY zotero.__addonRef__.pref.sync.period.label "自动同步周期 (秒)">
<!ENTITY zotero.__addonRef__.pref.sync.manager.label "打开同步管理器">
<!ENTITY zotero.__addonRef__.pref.template.title "模板">
<!ENTITY zotero.__addonRef__.pref.template.editor.label "打开模板编辑器">
<!ENTITY zotero.__addonRef__.pref.annotation.title "PDF批注">
<!ENTITY zotero.__addonRef__.pref.annotation.autoAnnotation.label "自动添加新批注到工作区笔记">
<!ENTITY zotero.__addonRef__.workspace.menu.new "创建新主笔记">
<!ENTITY zotero.__addonRef__.workspace.menu.open "打开主笔记...">
<!ENTITY zotero.__addonRef__.workspace.menu.openWindow "在独立窗口中打开工作区">
<!ENTITY zotero.__addonRef__.workspace.menu.export "导出主笔记...">
<!ENTITY zotero.__addonRef__.workspace.menu.sync "同步主笔记">
<!ENTITY zotero.__addonRef__.workspace.menu.syncmanager "Better Notes同步管理">
<!ENTITY zotero.__addonRef__.workspace.menu.insertNotes "插入多条笔记...">
<!ENTITY zotero.__addonRef__.workspace.menu.insertTextTemplate "插入笔记模板[文本/Text]">
<!ENTITY zotero.__addonRef__.workspace.menu.insertItemTemplate "插入笔记模板[条目/Item]">
<!ENTITY zotero.__addonRef__.workspace.menu.editTemplate "笔记模板编辑器">
<!ENTITY zotero.__addonRef__.workspace.menu.citeSetting "笔记引用格式">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRSetting "注释公式识别">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRBing "Bing">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRMathpix "Mathpix">
<!ENTITY zotero.__addonRef__.workspace.menu.OCRXunfei "讯飞">
<!ENTITY zotero.__addonRef__.workspace.menu.addheading "插入标题...">
<!ENTITY zotero.__addonRef__.workspace.menu.indent "降低标题层级">
<!ENTITY zotero.__addonRef__.workspace.menu.unindent "提升标题层级">
<!ENTITY zotero.__addonRef__.workspace.menu.importLink "导入选中的笔记链接内容">
<!ENTITY zotero.__addonRef__.workspace.menu.updateLink "更新选中的笔记链接文本">
<!ENTITY zotero.__addonRef__.workspace.menu.autoannotation "自动插入新注释到笔记">
<!ENTITY zotero.__addonRef__.workspace.menu.convertmd "转换剪贴板中的MarkDown内容">
<!ENTITY zotero.__addonRef__.workspace.menu.convertasciidoc "转换剪贴板中的AsciiDoc内容">
<!ENTITY zotero.__addonRef__.workspace.menu.treeview "大纲: 树视图">
<!ENTITY zotero.__addonRef__.workspace.menu.mindmap "大纲: 思维导图">
<!ENTITY zotero.__addonRef__.workspace.menu.bubblemap "大纲: 气泡导图">
<!ENTITY zotero.__addonRef__.workspace.menu.guide "Better Notes用户指引">
<!ENTITY zotero.__addonRef__.workspace.menu.about "关于Better Notes">
<!ENTITY zotero.__addonRef__.itemmenu.setMainNote.label "设置为主笔记">
<!ENTITY zotero.__addonRef__.itemmenu.exportNote.label "导出笔记及条目子笔记">
<!ENTITY zotero.__addonRef__.itemmenu.exportNotes.label "导出笔记及条目子笔记为Markdown">
<!ENTITY zotero.__addonRef__.export.title "导出笔记">
<!ENTITY zotero.__addonRef__.export.cannotworkwith.label "无法一起使用:">
<!ENTITY zotero.__addonRef__.export.workwith.label "只在此时有效:">
<!ENTITY zotero.__addonRef__.export.option.label "通用">
<!ENTITY zotero.__addonRef__.export.link.enable.label "嵌入链接的子笔记">
<!ENTITY zotero.__addonRef__.export.note.enable.label "导出到新笔记">
<!ENTITY zotero.__addonRef__.export.markdown.label "MarkDown">
<!ENTITY zotero.__addonRef__.export.file.enable.label "导出为MarkDown文件">
<!ENTITY zotero.__addonRef__.export.singlefile.enable.label "导出链接的子笔记为MarkDown文件">
<!ENTITY zotero.__addonRef__.export.enableautosync.enable.label "修改时自动同步到导出路径">
<!ENTITY zotero.__addonRef__.export.yaml.enable.label "YAML Front Matter">
<!ENTITY zotero.__addonRef__.export.document.label "文档">
<!ENTITY zotero.__addonRef__.export.docx.enable.label "导出到Word文档">
<!ENTITY zotero.__addonRef__.export.pdf.enable.label "导出到PDF文档">
<!ENTITY zotero.__addonRef__.export.mindmap.label "思维导图">
<!ENTITY zotero.__addonRef__.export.freemind.enable.label "FreeMind">
<!ENTITY zotero.__addonRef__.sync.title "同步状态">
<!ENTITY zotero.__addonRef__.sync.export.label "导出为...">
<!ENTITY zotero.__addonRef__.sync.path.label "同步到:">
<!ENTITY zotero.__addonRef__.sync.changesync.label "修改路径">
<!ENTITY zotero.__addonRef__.sync.lastsync.label "最近同步:">
<!ENTITY zotero.__addonRef__.sync.enable.label "保持同步">
<!ENTITY zotero.__addonRef__.sync.dosync.label "立刻同步">
<!ENTITY zotero.__addonRef__.sync.synclist.label "同步管理">
<!ENTITY zotero.__addonRef__.syncList.title "同步管理">
<!ENTITY zotero.__addonRef__.syncList.notename.label "名称">
<!ENTITY zotero.__addonRef__.syncList.syncpath.label "同步到">
<!ENTITY zotero.__addonRef__.syncList.lastsync.label "最近同步">
<!ENTITY zotero.__addonRef__.syncList.doupdate.label "刷新">
<!ENTITY zotero.__addonRef__.syncList.changesyncperiod.label "同步周期:">
<!ENTITY zotero.__addonRef__.syncList.dosync.label "同步">
<!ENTITY zotero.__addonRef__.syncList.changesync.label "修改路径">
<!ENTITY zotero.__addonRef__.syncList.removesync.label "取消同步">
<!ENTITY zotero.__addonRef__.syncList.related.label "应用于关联笔记">
<!ENTITY zotero.__addonRef__.wizard.title "欢迎使用 Zotero Better Notes">
<!ENTITY zotero.__addonRef__.wizard.page1.header "Zotero Better Notes 用户指引">
<!ENTITY zotero.__addonRef__.wizard.page1.description "单击下一步以继续。">
<!ENTITY zotero.__addonRef__.wizard.open "Knowledge工作区">
<!ENTITY zotero.__addonRef__.wizard.page3.header "Knowledge工作区">
<!ENTITY zotero.__addonRef__.wizard.page3.description "单击“我的文库”上方链接或笔记编辑器中的“主页”按钮,来打开或聚焦工作区。">
<!ENTITY zotero.__addonRef__.wizard.layout "Knowledge工作区布局">
<!ENTITY zotero.__addonRef__.wizard.page4.header "大纲/主笔记/链接笔记预览">
<!ENTITY zotero.__addonRef__.wizard.page4.description1 "用大纲修改/跳转主笔记的标题;">
<!ENTITY zotero.__addonRef__.wizard.page4.description2 "在阅读时可以从其他笔记添加链接到主笔记。支持导出包含链接全文的MarkDown/HTML。">
<!ENTITY zotero.__addonRef__.wizard.page4.description3 "在预览中打开并编辑链接的笔记。">
<!ENTITY zotero.__addonRef__.wizard.link "添加笔记链接">
<!ENTITY zotero.__addonRef__.wizard.page5.header "随写随记,并在任何地方打开笔记链接。">
<!ENTITY zotero.__addonRef__.wizard.page5.description1 "单击按钮,在主笔记中当前行插入当前笔记的链接。">
<!ENTITY zotero.__addonRef__.wizard.page5.description2 "通过笔记链接,甚至可以从其他应用程序跳转到笔记!">
<!ENTITY zotero.__addonRef__.wizard.setup "配置工作区">
<!ENTITY zotero.__addonRef__.wizard.page6.header "即将完成。在开始之前,建议按照指引快速配置并上手。">
<!ENTITY zotero.__addonRef__.wizard.page6.description1 "您可以稍后从工作区左侧'💡'返回这里。">
<!ENTITY zotero.__addonRef__.wizard.page6.description2 "提示:如果未设置主笔记,此插件将无法正常工作。">
<!ENTITY zotero.__addonRef__.wizard.setup.enable.label "启用快速配置">
<!ENTITY zotero.__addonRef__.wizard.setup.collectionenable.label "创建新的分类用来存放主笔记:">
<!ENTITY zotero.__addonRef__.wizard.setup.noteenable.label "创建 'Zotero Better Notes 用户指引:工作流' 笔记">
<!ENTITY zotero.__addonRef__.template.title "模板编辑器">
<!ENTITY zotero.__addonRef__.template.header.label "模板名称">
<!ENTITY zotero.__addonRef__.template.list.label "模板列表">
<!ENTITY zotero.__addonRef__.template.create.label "新建">
<!ENTITY zotero.__addonRef__.template.import.label "从笔记新建...">
<!ENTITY zotero.__addonRef__.template.save.label "保存">
<!ENTITY zotero.__addonRef__.template.delete.label "删除">
<!ENTITY zotero.__addonRef__.template.reset.label "重置">
<!ENTITY zotero.__addonRef__.template.more.label "获取更多模板">
<!ENTITY zotero.__addonRef__.help.feedback.caption.label "用户指引和反馈">
<!ENTITY zotero.__addonRef__.help.feedback.label "GitHub">
<!ENTITY zotero.__addonRef__.help.doc.caption.label "使用文档">
<!ENTITY zotero.__addonRef__.help.doc.label "Better Notes手册(中文)">
<!ENTITY zotero.__addonRef__.help.version.label "VERSION __buildVersion__">
<!ENTITY zotero.__addonRef__.help.version.label "__addonName__ 版本 __buildVersion__">
<!ENTITY zotero.__addonRef__.help.releasetime.label "Build __buildTime__">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

View File

@ -1,26 +0,0 @@
pref("extensions.zotero.Knowledge4Zotero.recentMainNoteIds", "");
pref("extensions.zotero.Knowledge4Zotero.syncNoteIds", "");
pref("extensions.zotero.Knowledge4Zotero.syncPeriod", 30000);
pref("extensions.zotero.Knowledge4Zotero.autoAnnotation", false);
pref("extensions.zotero.Knowledge4Zotero.exportMD", true);
pref("extensions.zotero.Knowledge4Zotero.exportSubMD", false);
pref("extensions.zotero.Knowledge4Zotero.exportAutoSync", false);
pref("extensions.zotero.Knowledge4Zotero.exportYAMLHeader", true);
pref("extensions.zotero.Knowledge4Zotero.embedLink", true);
pref("extensions.zotero.Knowledge4Zotero.exportNote", false);
pref("extensions.zotero.Knowledge4Zotero.exportDocx", false);
pref("extensions.zotero.Knowledge4Zotero.exportPDF", false);
pref("extensions.zotero.Knowledge4Zotero.exportFreeMind", false);
pref("extensions.zotero.Knowledge4Zotero.OCREngine", "bing");
pref("extensions.zotero.Knowledge4Zotero.OCRMathpix.Appid", "");
pref("extensions.zotero.Knowledge4Zotero.OCRMathpix.Appkey", "");
pref("extensions.zotero.Knowledge4Zotero.OCRXunfei.APPID", "");
pref("extensions.zotero.Knowledge4Zotero.OCRMathpix.APISecret", "");
pref("extensions.zotero.Knowledge4Zotero.OCRMathpix.APIKey", "");
pref("extensions.zotero.Knowledge4Zotero.linkAction.click", "");
pref("extensions.zotero.Knowledge4Zotero.linkAction.shiftclick", "");
pref("extensions.zotero.Knowledge4Zotero.linkAction.ctrlclick", "");
pref("extensions.zotero.Knowledge4Zotero.linkAction.altclick", "");
pref("extensions.zotero.Knowledge4Zotero.linkAction.preview", true);
pref("extensions.zotero.Knowledge4Zotero.imagePreview.ctrlclick", true);
pref("extensions.zotero.Knowledge4Zotero.imagePreview.dblclick", true);

View File

@ -12,21 +12,22 @@
em:creator="__author__"
em:description="__description__"
em:homepageURL="__homepage__"
em:iconURL="chrome://__addonRef__/skin/favicon.png"
em:updateURL="https://raw.githubusercontent.com/windingwind/zotero-better-notes/master/update.rdf">>
<em:type>2</em:type>
<em:targetApplication RDF:resource="rdf:#$x61SL3"/>
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>6.0.14-beta</em:minVersion>
<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>6.0.14-beta</em:minVersion>
<em:minVersion>5.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>

19
addon/manifest.json Normal file
View 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.999",
"strict_max_version": "7.0.*"
}
}
}

26
addon/prefs.js Normal file
View File

@ -0,0 +1,26 @@
pref("__prefsPrefix__.recentMainNoteIds", "");
pref("__prefsPrefix__.syncNoteIds", "");
pref("__prefsPrefix__.syncPeriodSeconds", 30);
pref("__prefsPrefix__.autoAnnotation", false);
pref("__prefsPrefix__.embedLink", true);
pref("__prefsPrefix__.standaloneLink", false);
pref("__prefsPrefix__.keepLink", true);
pref("__prefsPrefix__.exportMD", true);
pref("__prefsPrefix__.setAutoSync", false);
pref("__prefsPrefix__.withYAMLHeader", false);
pref("__prefsPrefix__.exportDocx", false);
pref("__prefsPrefix__.exportPDF", false);
pref("__prefsPrefix__.exportFreeMind", false);
pref("__prefsPrefix__.exportNote", false);
pref("__prefsPrefix__.OCREngine", "bing");
pref("__prefsPrefix__.OCRMathpix.Appid", "");
pref("__prefsPrefix__.OCRMathpix.Appkey", "");
pref("__prefsPrefix__.OCRXunfei.APPID", "");
pref("__prefsPrefix__.OCRMathpix.APISecret", "");
pref("__prefsPrefix__.OCRMathpix.APIKey", "");
pref("__prefsPrefix__.workspace.outline.expandLevel", 2);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -1,18 +1,30 @@
{
"name": "zotero-better-notes",
"version": "0.8.8",
"description": "Everything about note management. All in Zotero.",
"config": {
"addonName": "Zotero Better Notes",
"addonID": "Knowledge4Zotero@windingwind.com",
"addonRef": "Knowledge4Zotero",
"version": "0.8.9",
"description": "Everything about note management. All in Zotero.",
"main": "src/index.js",
"addonRef": "BetterNotes",
"prefsPrefix": "extensions.zotero.Knowledge4Zotero",
"addonInstance": "BetterNotes",
"updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-better-notes/bootstrap/update.json"
},
"main": "src/index.ts",
"scripts": {
"build-dev": "cross-env NODE_ENV=development node build.js",
"build": "cross-env NODE_ENV=production node build.js",
"start": "node start.js",
"stop": "node stop.js",
"prerestart": "npm run build-dev",
"restart": "node restart.js",
"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-dev": "npm run build-dev && npm run stop && npm run start",
"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"
},
@ -35,8 +47,6 @@
"hast-util-to-text": "^3.1.1",
"hastscript": "^7.1.0",
"html-docx-js-typescript": "^0.1.5",
"prosemirror-model": "^1.18.3",
"prosemirror-transform": "^1.7.0",
"rehype-format": "^4.0.1",
"rehype-parse": "^8.0.4",
"rehype-remark": "^9.1.2",
@ -53,16 +63,26 @@
"unist-util-visit": "^4.1.1",
"unist-util-visit-parents": "^5.1.1",
"yamljs": "^0.3.0",
"zotero-plugin-toolkit": "^0.1.1"
"zotero-plugin-toolkit": "file:../zotero-plugin-toolkit"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/jquery": "^3.5.14",
"@types/node": "^17.0.31",
"compressing": "^1.5.1",
"@types/browser-or-node": "^1.3.0",
"@types/diff": "^5.0.3",
"@types/node": "^18.11.17",
"@types/seedrandom": "^3.0.5",
"@types/yamljs": "^0.2.31",
"compressing": "^1.6.3",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"esbuild": "^0.14.34",
"release-it": "^14.14.0",
"zotero-types": "^1.0.9"
"esbuild": "^0.17.4",
"minimist": "^1.2.7",
"prosemirror-model": "^1.18.3",
"prosemirror-state": "^1.4.2",
"prosemirror-transform": "^1.7.1",
"prosemirror-view": "^1.30.2",
"release-it": "^15.6.0",
"replace-in-file": "^6.3.5",
"typescript": "^4.9.4",
"zotero-types": "^1.0.12"
}
}

View File

@ -1,8 +0,0 @@
const { execSync } = require("child_process");
const { killZotero, startZotero } = require("./zotero-cmd.json");
try {
execSync(killZotero);
} catch (e) {}
execSync(startZotero);

View File

@ -9,11 +9,9 @@ const {
author,
description,
homepage,
addonName,
addonID,
addonRef,
version,
} = require("./package.json");
config,
} = require("../package.json");
function copyFileSync(source, target) {
var targetFile = target;
@ -87,63 +85,72 @@ async function main() {
const buildDir = "builds";
console.log(
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}`
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
process.env.NODE_ENV,
]}`
);
clearFolder(buildDir);
copyFolderRecursiveSync("addon", buildDir);
const buildStartTime = new Date().getTime();
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,
__env__: `"${process.env.NODE_ENV}"`,
},
bundle: true,
// Entry should be the same as addon/chrome/content/overlay.xul
outfile: path.join(buildDir, "addon/chrome/content/scripts/index.js"),
// Don't turn minify on
// minify: true,
target: ["firefox60"],
})
.catch(() => process.exit(1));
console.log(
`[Build] Run esbuild OK in ${
(new Date().getTime() - buildStartTime) / 1000
} s.`
);
await esbuild
.build({
entryPoints: ["src/editor/editorScript.ts"],
entryPoints: ["src/extras/editorScript.ts"],
bundle: true,
outfile: path.join(buildDir, "temp/editorScript.js"),
// minify: true,
outfile: path.join(
buildDir,
"addon/chrome/content/scripts/editorScript.js"
),
target: ["firefox60"],
})
.catch(() => process.exit(1));
const optionsScript = {
files: [path.join(buildDir, "addon/chrome/content/scripts/index.js")],
from: [/__placeholder:editorScript.js__/g],
to: [
fs
.readFileSync(path.join(buildDir, "temp/editorScript.js"))
.toString()
.replace(/\\/g, "\\\\"),
],
countMatches: true,
};
await esbuild
.build({
entryPoints: ["src/extras/docxWorker.ts"],
bundle: true,
outfile: path.join(
buildDir,
"addon/chrome/content/scripts/docxWorker.js"
),
target: ["firefox60"],
})
.catch(() => process.exit(1));
_ = replace.sync(optionsScript);
console.log(
"[Build] Run replace in ",
_.filter((f) => f.hasChanged).map(
(f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`
)
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: [
@ -151,31 +158,17 @@ async function main() {
path.join(buildDir, "**/*.dtd"),
path.join(buildDir, "**/*.xul"),
path.join(buildDir, "**/*.html"),
path.join(buildDir, "**/*.manifest"),
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: [
/__author__/g,
/__description__/g,
/__homepage__/g,
/__addonName__/g,
/__addonID__/g,
/__addonRef__/g,
/__buildVersion__/g,
/__buildTime__/g,
/<em:version>\S*<\/em:version>/g,
],
to: [
author,
description,
homepage,
addonName,
addonID,
addonRef,
version,
buildTime,
`<em:version>${version}</em:version>`,
],
from: replaceFrom,
to: replaceTo,
countMatches: true,
};
@ -205,4 +198,7 @@ async function main() {
);
}
main();
main().catch((err) => {
console.log(err);
process.exit(1);
});

25
scripts/start.js Normal file
View 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
View 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) {}

View 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"
}
}

View File

@ -1,102 +1,179 @@
/*
* This file defines the plugin's structure.
*/
import ZoteroToolkit from "zotero-plugin-toolkit/dist/index";
import {
ColumnOptions,
VirtualizedTableHelper,
} from "zotero-plugin-toolkit/dist/helpers/virtualizedTable";
import hooks from "./hooks";
import api from "./api";
import ZoteroEvents from "./zotero/events";
import ZoteroNotifies from "./zotero/notifies";
import ZoteroViews from "./zotero/views";
import ReaderViews from "./reader/readerViews";
import WizardWindow from "./wizard/wizardWindow";
import { TemplateController, TemplateAPI } from "./template/templateController";
import SyncInfoWindow from "./sync/syncInfoWindow";
import SyncListWindow from "./sync/syncListWindow";
import SyncController from "./sync/syncController";
import WorkspaceWindow from "./workspace/workspaceWindow";
import WorkspaceOutline from "./workspace/workspaceOutline";
import WorkspaceMenu from "./workspace/workspaceMenu";
import NoteUtils from "./note/noteUtils";
import NoteParse from "./note/noteParse";
import NoteExportWindow from "./note/noteExportWindow";
import NoteExport from "./note/noteExportController";
import NoteImport from "./note/noteImportController";
import SyncDiffWindow from "./sync/syncDiffWindow";
import EditorViews from "./editor/editorViews";
import EditorController from "./editor/editorController";
import EditorImageViewer from "./editor/imageViewerWindow";
import TemplateWindow from "./template/templateWindow";
import { SyncUtils } from "./sync/syncUtils";
import ZoteroToolkit from "zotero-plugin-toolkit";
import AddonLocale from "./zotero/locale";
class BetterNotes {
public env: "development" | "production";
public Locale: AddonLocale;
public ZoteroEvents: ZoteroEvents;
public ZoteroNotifies: ZoteroNotifies;
// Zotero UI
public ZoteroViews: ZoteroViews;
// Reader UI
public ReaderViews: ReaderViews;
// Workspace UI
public WorkspaceOutline: WorkspaceOutline;
public WorkspaceWindow: WorkspaceWindow;
public WorkspaceMenu: WorkspaceMenu;
// First-run wizard
public WizardWindow: WizardWindow;
// Sync tools
public SyncUtils: SyncUtils;
public SyncInfoWindow: SyncInfoWindow;
public SyncListWindow: SyncListWindow;
public SyncController: SyncController;
// Template
public TemplateWindow: TemplateWindow;
public TemplateController: TemplateController;
// Just for template API consistency
public knowledge: TemplateAPI;
// Note tools
public NoteUtils: NoteUtils;
public NoteExport: NoteExport;
public NoteImport: NoteImport;
public SyncDiffWindow: SyncDiffWindow;
public NoteExportWindow: NoteExportWindow;
public NoteParse: NoteParse;
public EditorViews: EditorViews;
public EditorController: EditorController;
public EditorImageViewer: EditorImageViewer;
public toolkit: ZoteroToolkit;
class Addon {
public data: {
alive: boolean;
// Env type, see build.js
env: "development" | "production";
// ztoolkit: MyToolkit;
ztoolkit: ZoteroToolkit;
locale?: {
stringBundle: any;
};
prefs?: {
window: Window;
columns: Array<ColumnOptions>;
rows: Array<{ [dataKey: string]: string }>;
};
export: {
pdf: { promise?: _ZoteroTypes.PromiseObject };
docx: { worker?: HTMLIFrameElement };
};
sync: {
lock: boolean;
manager: {
window?: Window;
tableHelper?: VirtualizedTableHelper;
data: {
noteId: number;
noteName: string;
lastSync: string;
filePath: string;
}[];
};
diff: {
window?: Window;
};
};
notify: Array<Parameters<_ZoteroTypes.Notifier.Notify>>;
workspace: {
mainId: number;
previewId: number;
tab: {
active: boolean;
id?: string;
container?: XUL.Box;
};
window: {
active: boolean;
window?: Window;
container?: XUL.Box;
};
outline: OutlineType;
};
imageViewer: {
window?: Window;
srcList: string[];
idx: number;
scaling: number;
title: string;
pined: boolean;
anchorPosition?: {
left: number;
top: number;
};
};
templateEditor: {
window?: Window;
tableHelper?: VirtualizedTableHelper;
templates: { name: string }[];
};
templatePicker: {
noteId?: number;
lineIndex?: number;
};
prompt?: Prompt;
} = {
alive: true,
env: __env__,
// ztoolkit: new MyToolkit(),
ztoolkit: new ZoteroToolkit(),
export: {
pdf: { promise: undefined },
docx: { worker: undefined },
},
sync: {
lock: false,
manager: {
data: [],
},
diff: {},
},
notify: [],
workspace: {
get mainId(): number {
return parseInt(getPref("mainKnowledgeID") as string);
},
set mainId(id: number) {
setPref("mainKnowledgeID", id);
},
previewId: -1,
tab: {
active: false,
},
window: {
active: false,
},
outline: OutlineType.treeView,
},
imageViewer: {
window: undefined,
srcList: [],
idx: -1,
scaling: 1,
title: "Note",
pined: false,
anchorPosition: undefined,
},
templateEditor: {
window: undefined,
tableHelper: undefined,
templates: [],
},
templatePicker: {},
};
// Lifecycle hooks
public hooks: typeof hooks;
// APIs
public api: typeof api;
constructor() {
this.Locale = new AddonLocale(this);
this.ZoteroEvents = new ZoteroEvents(this);
this.ZoteroNotifies = new ZoteroNotifies(this);
this.ZoteroViews = new ZoteroViews(this);
this.ReaderViews = new ReaderViews(this);
this.WorkspaceOutline = new WorkspaceOutline(this);
this.WorkspaceWindow = new WorkspaceWindow(this);
this.WorkspaceMenu = new WorkspaceMenu(this);
this.EditorViews = new EditorViews(this);
this.EditorController = new EditorController(this);
this.EditorImageViewer = new EditorImageViewer(this);
this.WizardWindow = new WizardWindow(this);
this.SyncUtils = new SyncUtils(this);
this.SyncInfoWindow = new SyncInfoWindow(this);
this.SyncListWindow = new SyncListWindow(this);
this.SyncController = new SyncController(this);
this.SyncDiffWindow = new SyncDiffWindow(this);
this.TemplateWindow = new TemplateWindow(this);
this.TemplateController = new TemplateController(this);
this.NoteUtils = new NoteUtils(this);
this.NoteExport = new NoteExport(this);
this.NoteImport = new NoteImport(this);
this.NoteExportWindow = new NoteExportWindow(this);
this.NoteParse = new NoteParse(this);
this.knowledge = new TemplateAPI(this);
this.toolkit = new ZoteroToolkit();
// Disable since we are still using overlay
this.toolkit.UI.enableElementRecordGlobal = false;
this.hooks = hooks;
this.api = api;
}
}
export default BetterNotes;
/**
* 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 { UITool } from "zotero-plugin-toolkit/dist/tools/ui";
import { PreferencePaneManager } from "zotero-plugin-toolkit/dist/managers/preferencePane";
import { getPref, setPref } from "./utils/prefs";
import { OutlineType } from "./utils/workspace";
import { Prompt } from "zotero-plugin-toolkit/dist/managers/prompt";
export class MyToolkit extends BasicTool {
UI: UITool;
PreferencePane: PreferencePaneManager;
constructor() {
super();
this.UI = new UITool(this);
this.PreferencePane = new PreferencePaneManager(this);
}
unregisterAll() {
unregister(this);
}
}
export default Addon;

106
src/api.ts Normal file
View File

@ -0,0 +1,106 @@
// workspace
import {
getWorkspaceEditor,
initWorkspace,
toggleNotesPane,
toggleOutlinePane,
togglePreviewPane,
} from "./modules/workspace/content";
const workspace = {
getWorkspaceEditor,
initWorkspace,
toggleNotesPane,
toggleOutlinePane,
togglePreviewPane,
};
// sync
import sync = require("./modules/sync/api");
// convert
import convert = require("./modules/convert/api");
// template
import { runTemplate, runItemTemplate } from "./modules/template/api";
import {
SYSTEM_TEMPLATE_NAMES,
DEFAULT_TEMPLATES,
} from "./modules/template/data";
import {
getTemplateKeys,
getTemplateText,
setTemplate,
initTemplates,
removeTemplate,
} from "./modules/template/controller";
import { renderTemplatePreview } from "./modules/template/preview";
import {
showTemplatePicker,
updateTemplatePicker,
} from "./modules/template/picker";
const template = {
SYSTEM_TEMPLATE_NAMES,
DEFAULT_TEMPLATES,
runTemplate,
runItemTemplate,
getTemplateKeys,
getTemplateText,
setTemplate,
initTemplates,
removeTemplate,
renderTemplatePreview,
updateTemplatePicker,
};
// export
import { exportNotes } from "./modules/export/api";
import { saveMD, syncMDBatch } from "./modules/export/markdown";
import { saveDocx } from "./modules/export/docx";
import { saveFreeMind } from "./modules/export/freemind";
const _export = {
exportNotes,
saveMD,
syncMDBatch,
saveFreeMind,
saveDocx,
savePDF,
};
// import
import { fromMD } from "./modules/import/markdown";
const _import = {
fromMD,
};
// window
import { showImageViewer } from "./modules/imageViewer";
import { showSyncInfo } from "./modules/sync/infoWindow";
import { showExportNoteOptions } from "./modules/export/exportWindow";
import { showTemplateEditor } from "./modules/template/editorWindow";
import { showSyncManager } from "./modules/sync/managerWindow";
import { showSyncDiff } from "./modules/sync/diffWindow";
import { savePDF } from "./modules/export/pdf";
const window = {
showImageViewer,
showExportNoteOptions,
showSyncInfo,
showSyncManager,
showSyncDiff,
showTemplateEditor,
showTemplatePicker,
};
export default {
workspace,
_export,
_import,
window,
sync,
convert,
template,
};

View File

@ -1,73 +0,0 @@
/*
* This file realizes editor watch.
*/
import BetterNotes from "../addon";
import AddonBase from "../module";
class EditorController extends AddonBase {
editorHistory: Array<{
instance: Zotero.EditorInstance;
time: number;
}>;
editorPromise: _ZoteroTypes.PromiseObject;
activeEditor: Zotero.EditorInstance;
constructor(parent: BetterNotes) {
super(parent);
this.editorHistory = [];
}
startWaiting() {
this.editorPromise = Zotero.Promise.defer();
}
async waitForEditor() {
await this.editorPromise.promise;
}
injectScripts(_window: Window) {
if (!_window.document.getElementById("betternotes-script")) {
const messageScript = this._Addon.toolkit.UI.createElement(
_window.document,
"script"
) as HTMLScriptElement;
messageScript.id = "betternotes-script";
messageScript.innerHTML = `__placeholder:editorScript.js__`;
_window.document.head.append(messageScript);
}
_window.addEventListener("BNMessage", (e: CustomEvent) => {
this._Addon.toolkit.Tool.log("note editor event", e.detail);
switch (e.detail.type) {
case "exportPDFDone":
this._Addon.NoteExport._pdfPrintPromise.resolve();
break;
case "exportDocxDone":
this._Addon.NoteExport._docxBlob = e.detail.docxBlob;
this._Addon.NoteExport._docxPromise.resolve();
break;
default:
break;
}
});
}
recordEditor(instance: Zotero.EditorInstance) {
this.editorHistory.push({
instance: instance,
time: new Date().getTime(),
});
const aliveInstances = Zotero.Notes._editorInstances.map(
(_i) => _i.instanceID
);
this.editorHistory = this.editorHistory.filter((obj) =>
aliveInstances.includes(obj.instance.instanceID)
);
if (this.editorPromise) {
this.editorPromise.resolve();
}
}
}
export default EditorController;

View File

@ -1,225 +0,0 @@
// DO NOT USE BACKTICK IN THIS FILE
const { Fragment, Slice } = require("prosemirror-model");
const { Step, StepResult } = require("prosemirror-transform");
import { asBlob } from "html-docx-js-typescript";
class SetAttrsStep extends Step {
// :: (number, Object | null)
constructor(pos, attrs) {
super();
this.pos = pos;
this.attrs = attrs;
}
apply(doc) {
let target = doc.nodeAt(this.pos);
if (!target) return StepResult.fail("No node at given position");
let newNode = target.type.create(this.attrs, Fragment.emtpy, target.marks);
let slice = new Slice(Fragment.from(newNode), 0, target.isLeaf ? 0 : 1);
return StepResult.fromReplace(doc, this.pos, this.pos + 1, slice);
}
invert(doc) {
let target = doc.nodeAt(this.pos);
return new SetAttrsStep(this.pos, target ? target.attrs : null);
}
map(mapping) {
let pos = mapping.mapResult(this.pos, 1);
return pos.deleted ? null : new SetAttrsStep(pos.pos, this.attrs);
}
toJSON() {
return { stepType: "setAttrs", pos: this.pos, attrs: this.attrs };
}
static fromJSON(schema, json) {
if (
typeof json.pos != "number" ||
(json.attrs != null && typeof json.attrs != "object")
)
throw new RangeError("Invalid input for SetAttrsStep.fromJSON");
return new SetAttrsStep(json.pos, json.attrs);
}
}
// @ts-ignore
window.SetAttrsStep = SetAttrsStep;
// @ts-ignore
window.updateImageDimensions = function (
nodeID,
width,
height,
state,
dispatch
) {
let { tr } = state;
console.log(nodeID, width, height, state, dispatch);
state.doc.descendants((node, pos) => {
if (node.type.name === "image" && node.attrs.nodeID === nodeID) {
tr.step(new SetAttrsStep(pos, { ...node.attrs, width, height }));
tr.setMeta("addToHistory", false);
tr.setMeta("system", true);
dispatch(tr);
return false;
}
});
};
window.addEventListener(
"message",
async (e) => {
console.log(e);
const editNode = document.querySelector(".primary-editor") as HTMLElement;
let t = 0;
let imageFlag = false;
while (!imageFlag && t < 500) {
await new Promise(function (resolve) {
setTimeout(resolve, 10);
});
imageFlag = !Array.prototype.find.call(
document.querySelectorAll("img"),
(e) => !e.getAttribute("src") || e.style.display === "none"
);
t += 1;
}
switch (e.data.type) {
case "exportPDF":
console.log("exportPDF");
const container = document.getElementById(
"editor-container"
) as HTMLElement;
container.style.display = "none";
const fullPageStyle = document.createElement("style");
fullPageStyle.innerHTML =
"@page { margin: 0; } @media print{ body { height : auto; -webkit-print-color-adjust: exact; color-adjust: exact; }}";
document.body.append(fullPageStyle);
const printNode = editNode.cloneNode(true) as HTMLElement;
printNode.style.padding = "20px";
document.body.append(printNode);
let printFlag = false;
window.onafterprint = (_e) => {
console.log("Print Dialog Closed..");
printFlag = true;
// document.title = "Printed";
window.dispatchEvent(
new CustomEvent("BNMessage", {
detail: {
type: "exportPDFDone",
},
})
);
};
window.onmouseover = (e) => {
if (printFlag) {
document.title = "Printed";
printNode.remove();
container.style.removeProperty("display");
}
};
document.title = (printNode?.firstChild as HTMLElement).innerText;
console.log(document.title);
window.print();
break;
case "exportDocx":
// @ts-ignore
const docxBlob: Blob = await window.getDocx(editNode);
console.log(docxBlob);
window.dispatchEvent(
new CustomEvent("BNMessage", {
detail: {
type: "exportDocxDone",
docxBlob: docxBlob,
},
})
);
break;
case "resizeImage":
console.log("resizeImage");
// @ts-ignore
window.updateImageDimensions(
// @ts-ignore
_currentEditorInstance._editorCore.view.state.selection.node.attrs
.nodeID,
e.data.width,
undefined,
// @ts-ignore
_currentEditorInstance._editorCore.view.state,
// @ts-ignore
_currentEditorInstance._editorCore.view.dispatch
);
break;
default:
break;
}
},
false
);
// @ts-ignore
window.getDocx = async (
dom: HTMLElement,
config: object,
{ title = document.title, width = undefined } = {}
) => {
if (!dom) return;
config = config || {};
let copyDom = document.createElement("span");
// const styleDom = document.querySelectorAll('style, link, meta')
const titleDom = document.createElement("title");
titleDom.innerText = title;
copyDom.appendChild(titleDom);
// Array.from(styleDom).forEach(item => {
// copyDom.appendChild(item.cloneNode(true))
// })
const cloneDom = dom.cloneNode(true) as HTMLElement;
if (width) {
const domTables = cloneDom.getElementsByTagName("table");
if (domTables.length) {
for (const table of domTables) {
table.style.width = width + "px";
}
}
}
copyDom.appendChild(cloneDom);
const htmlTemp = copyDom.innerHTML;
copyDom = null;
const iframeDom = document.createElement("iframe");
const attrObj = {
height: 0,
width: 0,
border: 0,
wmode: "Opaque",
};
const styleObj = {
position: "absolute",
top: "-999px",
left: "-999px",
};
Object.entries(attrObj).forEach(([key, value]) => {
iframeDom.setAttribute(key, String(value));
});
Object.entries(styleObj).forEach(([key, value]) => {
iframeDom.style[key] = value;
});
document.body.insertBefore(iframeDom, document.body.children[0]);
const iframeWin = iframeDom.contentWindow;
const iframeDocs = iframeWin.document;
iframeDocs.write("<!doctype html>");
iframeDocs.write(htmlTemp);
let htmlDoc =
'<!DOCTYPE html>\n<html lang="en"><head><meta charset="UTF-8"></head>\n';
htmlDoc += iframeDocs.documentElement.innerHTML;
htmlDoc += "\n</html>";
var converted = await asBlob(htmlDoc, config);
console.log(converted);
document.body.removeChild(iframeDom);
return converted;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,293 +0,0 @@
/*
* This file contains image viewer for note editor.
*/
import BetterNotes from "../addon";
import AddonBase from "../module";
class EditorImageViewer extends AddonBase {
_window: Window;
scaling: number;
srcList: string[];
idx: number;
title: string;
pined: boolean;
anchorPosition: {
left: number;
top: number;
};
icons: any;
constructor(parent: BetterNotes) {
super(parent);
this.scaling = 1;
this.title = "Note";
this.pined = false;
this.icons = {
pin: `<svg t="1668685819555" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1445" width="18" height="18"><path d="M631.637333 178.432a64 64 0 0 1 19.84 13.504l167.616 167.786667a64 64 0 0 1-19.370666 103.744l-59.392 26.304-111.424 111.552-8.832 122.709333a64 64 0 0 1-109.098667 40.64l-108.202667-108.309333-184.384 185.237333-45.354666-45.162667 184.490666-185.344-111.936-112.021333a64 64 0 0 1 40.512-109.056l126.208-9.429333 109.44-109.568 25.706667-59.306667a64 64 0 0 1 84.181333-33.28z m-25.450666 58.730667l-30.549334 70.464-134.826666 135.04-149.973334 11.157333 265.408 265.6 10.538667-146.474667 136.704-136.874666 70.336-31.146667-167.637333-167.765333z" p-id="1446"></path></svg>`,
pined: `<svg t="1668685864340" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1624" width="18" height="18"><path d="M631.637333 178.432a64 64 0 0 1 19.84 13.504l167.616 167.786667a64 64 0 0 1-19.370666 103.744l-59.392 26.304-111.424 111.552-8.832 122.709333a64 64 0 0 1-109.098667 40.64l-108.202667-108.309333-184.384 185.237333-45.354666-45.162667 184.490666-185.344-111.936-112.021333a64 64 0 0 1 40.512-109.056l126.208-9.429333 109.44-109.568 25.706667-59.306667a64 64 0 0 1 84.181333-33.28z" p-id="1625"></path></svg>`,
};
}
async onInit(srcs: string[], idx: number, title: string, pined: boolean) {
if (!this._window || this._window.closed) {
this._window = window.open(
"chrome://Knowledge4Zotero/content/imageViewer.html",
"betternotes-note-imagepreview",
`chrome,centerscreen,resizable,status,width=500,height=550${
pined ? ",alwaysRaised=yes" : ""
}`
);
let t = 0;
// Wait for window
while (t < 500 && this._window.document.readyState !== "complete") {
await Zotero.Promise.delay(10);
t += 1;
}
const container = this._window.document.querySelector(
".container"
) as HTMLDivElement;
const img = this._window.document.querySelector(
"#image"
) as HTMLImageElement;
this._window.document
.querySelector("#left")
.addEventListener("click", (e) => {
this.setIndex("left");
});
this._window.document
.querySelector("#bigger")
.addEventListener("click", (e) => {
this.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
this.setScale(this.scaling * 1.1);
});
this._window.document
.querySelector("#smaller")
.addEventListener("click", (e) => {
this.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
this.setScale(this.scaling / 1.1);
});
this._window.document
.querySelector("#resetwidth")
.addEventListener("click", (e) => {
this.setScale(1);
});
this._window.document
.querySelector("#right")
.addEventListener("click", (e) => {
this.setIndex("right");
});
this._window.document
.querySelector("#copy")
.addEventListener("click", (e) => {
this._Addon.toolkit.Tool.getCopyHelper()
.addImage(this.srcList[this.idx])
.copy();
this._Addon.ZoteroViews.showProgressWindow(
"Better Notes",
"Image Copied."
);
});
this._window.document
.querySelector("#save")
.addEventListener("click", async (e) => {
let parts = this.srcList[this.idx].split(",");
if (!parts[0].includes("base64")) {
return;
}
let mime = parts[0].match(/:(.*?);/)[1];
let bstr = atob(parts[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
let ext = Zotero.MIME.getPrimaryExtension(mime, "");
const filename = await this._Addon.toolkit.Tool.openFilePicker(
Zotero.getString("noteEditor.saveImageAs"),
"save",
[[`Image(*.${ext})`, `*.${ext}`]],
`${Zotero.getString("fileTypes.image").toLowerCase()}.${ext}`
);
if (filename) {
await OS.File.writeAtomic(
this._Addon.NoteUtils.formatPath(filename),
u8arr
);
}
this._Addon.ZoteroViews.showProgressWindow(
"Better Notes",
`Image Saved to ${filename}`
);
});
this._window.document.querySelector("#pin").innerHTML =
this.icons[pined ? "pined" : "pin"];
this._window.document.querySelector("#pin-tooltip").innerHTML = pined
? "Unpin"
: "Pin";
this._window.document
.querySelector("#pin")
.addEventListener("click", (e) => {
this.setPin();
});
this._window.addEventListener("keydown", (e) => {
// ctrl+w or esc
if ((e.key === "w" && e.ctrlKey) || e.keyCode === 27) {
this._window.close();
}
this.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
if (e.keyCode === 37 || e.keyCode === 40) {
this.setIndex("left");
}
if (e.keyCode === 38 || e.keyCode === 39) {
this.setIndex("right");
}
if (e.key === "0") {
this.setScale(1);
} else if (e.keyCode === 107 || e.keyCode === 187 || e.key === "=") {
this.setScale(this.scaling * 1.1);
} else if (e.key === "-") {
this.setScale(this.scaling / 1.1);
}
});
this._window.addEventListener("wheel", async (e) => {
this.anchorPosition = {
left: e.pageX - container.offsetLeft,
top: e.pageY - container.offsetTop,
};
function normalizeWheelEventDirection(evt) {
let delta = Math.hypot(evt.deltaX, evt.deltaY);
const angle = Math.atan2(evt.deltaY, evt.deltaX);
if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
// All that is left-up oriented has to change the sign.
delta = -delta;
}
return delta;
}
const delta = normalizeWheelEventDirection(e);
if (e.ctrlKey) {
this.setScale(
this.scaling *
Math.pow(delta > 0 ? 1.1 : 1 / 1.1, Math.round(Math.abs(delta)))
);
} else if (e.shiftKey) {
container.scrollLeft -= delta * 10;
} else {
container.scrollLeft += e.deltaX * 10;
container.scrollTop += e.deltaY * 10;
}
});
img.addEventListener("mousedown", (e) => {
e.preventDefault();
// if (this.scaling <= 1) {
// return;
// }
img.onmousemove = (e) => {
e.preventDefault();
container.scrollLeft -= e.movementX;
container.scrollTop -= e.movementY;
};
img.onmouseleave = () => {
img.onmousemove = null;
img.onmouseup = null;
};
img.onmouseup = () => {
img.onmousemove = null;
img.onmouseup = null;
};
});
}
this.srcList = srcs;
this.idx = idx;
this.title = title || "Note";
this.pined = pined;
this.setImage();
this.setScale(1);
this._window.focus();
}
private setImage() {
(this._window.document.querySelector("#image") as HTMLImageElement).src =
this.srcList[this.idx];
this.setTitle();
(
this._window.document.querySelector(
"#left-container"
) as HTMLButtonElement
).style.opacity = this.idx === 0 ? "0.5" : "1";
(
this._window.document.querySelector(
"#right-container"
) as HTMLButtonElement
).style.opacity = this.idx === this.srcList.length - 1 ? "0.5" : "1";
}
private setIndex(type: "left" | "right") {
if (type === "left") {
this.idx > 0 ? (this.idx -= 1) : undefined;
}
if (type === "right") {
this.idx < this.srcList.length - 1 ? (this.idx += 1) : undefined;
}
this.setImage();
}
private setScale(scaling: number) {
const oldScale = this.scaling;
this.scaling = scaling;
if (this.scaling > 10) {
this.scaling = 10;
}
if (this.scaling < 0.1) {
this.scaling = 0.1;
}
const container = this._window.document.querySelector(
".container"
) as HTMLDivElement;
(
this._window.document.querySelector("#image") as HTMLImageElement
).style.width = `calc(100% * ${this.scaling})`;
if (this.scaling > 1) {
container.scrollLeft +=
this.anchorPosition.left * (this.scaling - oldScale);
container.scrollTop +=
this.anchorPosition.top * (this.scaling - oldScale);
}
(
this._window.document.querySelector(
"#bigger-container"
) as HTMLButtonElement
).style.opacity = this.scaling === 10 ? "0.5" : "1";
(
this._window.document.querySelector(
"#smaller-container"
) as HTMLButtonElement
).style.opacity = this.scaling === 0.1 ? "0.5" : "1";
// (
// this._window.document.querySelector("#image") as HTMLImageElement
// ).style.cursor = this.scaling <= 1 ? "default" : "move";
}
private setTitle() {
this._window.document.querySelector("title").innerText = `${this.idx + 1}/${
this.srcList.length
}:${this.title}`;
}
private setPin() {
this._window.close();
this.onInit(this.srcList, this.idx, this.title, !this.pined);
}
}
export default EditorImageViewer;

16
src/extras/docxWorker.ts Normal file
View File

@ -0,0 +1,16 @@
import { asBlob } from "html-docx-js-typescript";
// this runs in a webworker. accept input message
// and return output message
onmessage = ({ data: { type, jobId, message } }) => {
if (type === "parseDocx") {
console.log("DOCX Worker", type, jobId, message);
asBlob(message)
.then((blob) => {
postMessage({ type: "parseDocxReturn", jobId, message: blob }, "*");
})
.catch((err) => {
console.log(err);
});
}
};

339
src/extras/editorScript.ts Normal file
View File

@ -0,0 +1,339 @@
// The prosemirror imports are only for type hint
import { Node, NodeType } from "prosemirror-model";
import { Mark, MarkType, ResolvedPos, Attrs } from "prosemirror-model";
import { EditorState, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
declare const _currentEditorInstance: {
_editorCore: EditorCore;
};
// Code from https://github.com/ueberdosis/tiptap/tree/main/packages/core/src/helpers
function objectIncludes(object1: any, object2: any) {
const keys = Object.keys(object2);
if (!keys.length) {
return true;
}
return !!keys.filter((key) => object2[key] === object1[key]).length;
}
function findMarkInSet(
marks: readonly Mark[],
type: MarkType,
attributes = {}
) {
return marks.find((item) => {
return item.type === type && objectIncludes(item.attrs, attributes);
});
}
function isMarkInSet(marks: readonly Mark[], type: MarkType, attributes = {}) {
return !!findMarkInSet(marks, type, attributes);
}
function getMarkRange($pos: ResolvedPos, type: MarkType, attributes = {}) {
if (!$pos || !type) {
return;
}
const start = $pos.parent.childAfter($pos.parentOffset);
if (!start.node) {
return;
}
const mark = findMarkInSet(start.node.marks, type, attributes);
if (!mark) {
return;
}
let startIndex = $pos.index();
let startPos = $pos.start() + start.offset;
let endIndex = startIndex + 1;
let endPos = startPos + start.node.nodeSize;
findMarkInSet(start.node.marks, type, attributes);
while (
startIndex > 0 &&
mark.isInSet($pos.parent.child(startIndex - 1).marks)
) {
startIndex -= 1;
startPos -= $pos.parent.child(startIndex).nodeSize;
}
while (
endIndex < $pos.parent.childCount &&
isMarkInSet($pos.parent.child(endIndex).marks, type, attributes)
) {
endPos += $pos.parent.child(endIndex).nodeSize;
endIndex += 1;
}
return {
from: startPos,
to: endPos,
};
}
function getMarkRangeAtCursor(state: EditorState, type: MarkType) {
const { selection } = state;
const { $from, empty } = selection;
if (empty) {
const start = $from.parent.childAfter($from.parentOffset);
if (start.node) {
const mark = start.node.marks.find(
(mark) => mark.type.name === type.name
);
if (mark) {
return getMarkRange($from, type, mark.attrs);
}
}
}
return null;
}
function deleteRange(from: number, to: number) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr } = state;
console.log("Delete Node", from, to);
tr.delete(from, to);
dispatch(tr);
};
}
function deleteRangeAtCursor(searchType: MarkType) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const range = getMarkRangeAtCursor(state, searchType);
if (range) {
const from = range.from;
const to = range.to;
return deleteRange(from, to)(state, dispatch);
}
};
}
function replaceRange(
from: number,
to: number,
text: string | undefined,
type: MarkType,
attrs: Attrs | string
) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr } = state;
if (typeof attrs === "string") {
attrs = JSON.parse(attrs) as Attrs;
}
const node = state.schema.text(text || state.doc.textBetween(from, to), [
type.create(attrs),
]);
console.log("Replace Node", from, to, node);
tr.replaceWith(from, to, node);
dispatch(tr);
};
}
function replaceRangeNode(
from: number,
to: number,
text: string | undefined,
nodeType: NodeType,
nodeAttrs: Attrs | string,
markType?: MarkType,
markAttrs?: Attrs | string
) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr } = state;
if (typeof nodeAttrs === "string") {
nodeAttrs = JSON.parse(nodeAttrs) as Attrs;
}
if (typeof markAttrs === "string") {
markAttrs = JSON.parse(markAttrs) as Attrs;
}
const node = nodeType.create(
nodeAttrs,
state.schema.text(text || state.doc.textBetween(from, to)),
markType ? [markType.create(markAttrs)] : []
);
console.log("Replace Node", from, to, node);
tr.replaceWith(from, to, node);
dispatch(tr);
};
}
function replaceRangeAtCursor(
text: string | undefined,
type: MarkType,
attrs: Attrs | string,
searchType: MarkType
) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const range = getMarkRangeAtCursor(state, searchType);
if (range) {
const from = range.from;
const to = range.to;
return replaceRange(from, to, text, type, attrs)(state, dispatch);
}
};
}
function moveRange(from: number, to: number, delta: number) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr, selection } = state;
const _TextSelection =
selection.constructor as unknown as typeof TextSelection;
const slice = state.doc.slice(from, to);
console.log("Move Node", from, to, delta, slice);
tr.delete(from, to);
tr.insert(from + delta, slice.content);
tr.setSelection(_TextSelection.create(tr.doc, from + delta));
tr.scrollIntoView();
dispatch(tr);
};
}
function updateMarkRangeAtCursor(type: MarkType, attrs: Attrs) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr, selection, doc } = state;
let { from, to } = selection;
const { $from, empty } = selection;
if (empty) {
const range = getMarkRangeAtCursor(state, type);
if (range) {
from = range.from;
to = range.to;
}
}
const hasMark = doc.rangeHasMark(from, to, type);
if (hasMark) {
tr.removeMark(from, to, type);
}
tr.addStoredMark(type.create(attrs));
if (to > from) {
tr.addMark(from, to, type.create(attrs));
}
dispatch(tr);
};
}
function removeMarkRangeAtCursor(type: MarkType) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
const { tr, selection } = state;
let { from, to } = selection;
const { $from, empty } = selection;
if (empty) {
const range = getMarkRangeAtCursor(state, type);
if (range) {
from = range.from;
to = range.to;
}
}
tr.ensureMarks([]);
if (to > from) {
tr.removeMark(from, to, type);
}
dispatch(tr);
};
}
function getHeadingLevelInRange(from: number, to: number) {
return (state: EditorState) => {
let level = -1;
state.doc.nodesBetween(from, to, (node, pos) => {
if (node.type.name === "heading") {
level = node.attrs.level;
}
});
return level;
};
}
function updateHeadingsInRange(from: number, to: number, levelOffset: number) {
return (state: EditorState, dispatch: EditorView["dispatch"]) => {
let { tr } = state;
state.doc.nodesBetween(from, to, (node, pos) => {
if (node.type.name === "heading") {
tr = tr.setNodeMarkup(pos, state.schema.nodes.heading, {
level: node.attrs.level + levelOffset,
});
}
});
dispatch(tr);
};
}
function refocusEditor(callback: Function) {
let scrollTop = document.querySelector(".editor-core")!.scrollTop;
let input = document.createElement("input");
input.style.position = "absolute";
input.style.opacity = "0";
document.body.append(input);
input.focus();
input.offsetTop;
setTimeout(() => {
(document.querySelector(".primary-editor") as any).focus();
input.remove();
document.querySelector(".editor-core")!.scrollTop = scrollTop;
setTimeout(callback, 0);
}, 0);
}
function updateImageDimensions(
nodeID: string,
width: number,
height: number | undefined,
state: EditorState,
dispatch: EditorView["dispatch"]
) {
let { tr } = state;
state.doc.descendants((node: Node, pos: number) => {
if (node.type.name === "image" && node.attrs.nodeID === nodeID) {
// tr.step(new SetAttrsStep(pos, { ...node.attrs, width, height }));
// tr.setMeta("addToHistory", false);
// tr.setMeta("system", true);
tr.setNodeMarkup(
pos,
node.type,
{ ...node.attrs, width, height },
node.marks
);
dispatch(tr);
return false;
}
});
}
export const BetterNotesEditorAPI = {
deleteRange,
deleteRangeAtCursor,
replaceRange,
replaceRangeNode,
replaceRangeAtCursor,
moveRange,
updateMarkRangeAtCursor,
removeMarkRangeAtCursor,
refocusEditor,
updateImageDimensions,
getHeadingLevelInRange,
updateHeadingsInRange,
};
// @ts-ignore
window.BetterNotesEditorAPI = BetterNotesEditorAPI;

236
src/hooks.ts Normal file
View File

@ -0,0 +1,236 @@
import { config } from "../package.json";
import { initLocale } from "./utils/locale";
import { registerPrefsWindow } from "./modules/preferenceWindow";
import { registerNoteLinkProxyHandler } from "./modules/noteLink";
import { registerEditorInstanceHook } from "./modules/editor/initalize";
import { initTemplates } from "./modules/template/controller";
import { registerMenus } from "./modules/menu";
import {
activateWorkspaceTab,
deActivateWorkspaceTab,
registerWorkspaceTab,
TAB_TYPE,
} from "./modules/workspace/tab";
import {
initWorkspaceEditor,
togglePreviewPane,
updateOutline,
} from "./modules/workspace/content";
import { registerNotify } from "./modules/notify";
import { showWorkspaceWindow } from "./modules/workspace/window";
import {
checkReaderAnnotationButton,
registerReaderInitializer,
unregisterReaderInitializer,
} from "./modules/reader";
async function onStartup() {
await Promise.all([
Zotero.initializationPromise,
Zotero.unlockPromise,
Zotero.uiReadyPromise,
]);
initLocale();
ztoolkit.ProgressWindow.setIconURI(
"default",
`chrome://${config.addonRef}/content/icons/favicon.png`
);
registerNoteLinkProxyHandler();
registerNotify(["tab", "item"]);
registerEditorInstanceHook();
initTemplates();
registerMenus();
registerWorkspaceTab();
registerReaderInitializer();
registerPrefsWindow();
addon.api.sync.setSync();
}
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 funcion clear.
*/
function onNotify(
event: string,
type: string,
ids: number[] | string[],
extraData: { [key: string]: any }
) {
// Workspace tab select/unselect callback
if (event === "select" && type === "tab") {
if (extraData[ids[0]].type == TAB_TYPE) {
activateWorkspaceTab();
} else {
deActivateWorkspaceTab();
}
}
// Workspace main note update
if (event === "modify" && type === "item") {
if ((ids as number[]).includes(addon.data.workspace.mainId)) {
addon.data.workspace.tab.active &&
updateOutline(addon.data.workspace.tab.container!);
addon.data.workspace.window.active &&
updateOutline(addon.data.workspace.window.container!);
}
}
if (event === "modify" && type === "item") {
const modifiedNotes = Zotero.Items.get(ids).filter((item) => item.isNote());
if (modifiedNotes.length) {
addon.api.sync.doSync(modifiedNotes, {
quiet: true,
skipActive: true,
reason: "item-modify",
});
}
}
// Reader annotation buttons update
if (event === "add" && type === "item") {
const annotationItems = Zotero.Items.get(ids as number[]).filter((item) =>
item.isAnnotation()
);
if (annotationItems.length === 0) {
checkReaderAnnotationButton(annotationItems);
}
} else {
return;
}
}
/**
* This function is just an example of dispatcher for Preference UI events.
* Any operations should be placed in a function to keep this funcion clear.
* @param type event type
* @param data event data
*/
async function onPrefsEvent(type: string, data: { [key: string]: any }) {
switch (type) {
case "load":
// registerPrefsScripts(data.window);
break;
default:
return;
}
}
function onOpenNote(
noteId: number,
mode: "auto" | "preview" | "workspace" | "standalone" = "auto",
options: {
lineIndex?: number;
} = {}
) {
const noteItem = Zotero.Items.get(noteId);
if (!noteItem?.isNote()) {
ztoolkit.log(`onOpenNote: ${noteId} is not a note.`);
return;
}
if (mode === "auto") {
if (noteId === addon.data.workspace.mainId) {
mode = "workspace";
} else if (
addon.data.workspace.tab.active ||
addon.data.workspace.window.active
) {
mode = "preview";
} else {
mode = "standalone";
}
}
switch (mode) {
case "preview":
addon.hooks.onSetWorkspaceNote(noteId, "preview", options);
break;
case "workspace":
addon.hooks.onSetWorkspaceNote(noteId, "main", options);
case "standalone":
ZoteroPane.openNoteWindow(noteId);
break;
default:
break;
}
}
function onSetWorkspaceNote(
noteId: number,
type: "main" | "preview" = "main",
options: {
lineIndex?: number;
} = {}
) {
if (type === "main") {
addon.data.workspace.mainId = noteId;
addon.data.workspace.tab.active &&
updateOutline(addon.data.workspace.tab.container!);
addon.data.workspace.window.active &&
updateOutline(addon.data.workspace.window.container!);
}
if (addon.data.workspace.window.active) {
initWorkspaceEditor(
addon.data.workspace.window.container,
type,
noteId,
options
);
type === "preview" &&
togglePreviewPane(addon.data.workspace.window.container, "open");
addon.data.workspace.window.window?.focus();
}
if (addon.data.workspace.tab.active) {
initWorkspaceEditor(
addon.data.workspace.tab.container,
type,
noteId,
options
);
type === "preview" &&
togglePreviewPane(addon.data.workspace.tab.container, "open");
Zotero_Tabs.select(addon.data.workspace.tab.id!);
}
}
function onOpenWorkspace(type: "tab" | "window" = "tab") {
if (type === "window") {
if (addon.data.workspace.window.active) {
addon.data.workspace.window.window?.focus();
return;
}
showWorkspaceWindow();
return;
}
if (type === "tab") {
// selecting tab will auto load the workspace content
Zotero_Tabs.select(addon.data.workspace.tab.id!);
return;
}
}
// 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 maintian.
export default {
onStartup,
onShutdown,
onNotify,
onPrefsEvent,
onOpenNote,
onSetWorkspaceNote,
onOpenWorkspace,
};

View File

@ -1,18 +1,28 @@
/*
* This file is the esbuild entrance.
*/
import { BasicTool } from "zotero-plugin-toolkit/dist/basic";
import Addon from "./addon";
import { config } from "../package.json";
import BetterNotes from "./addon";
const basicTool = new BasicTool();
const addon = new BetterNotes();
Zotero.BetterNotes = addon;
// For compatibility
Zotero.Knowledge4Zotero = addon;
window.addEventListener(
"load",
async function (e) {
Zotero.BetterNotes.ZoteroEvents.onInit();
},
false
);
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.window = basicTool.getGlobal("window");
_globalThis.document = basicTool.getGlobal("document");
_globalThis.OS = basicTool.getGlobal("OS") as typeof OS;
_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";
ztoolkit.UI.basicOptions.ui.enableElementDOMLog =
addon.data.env === "development";
ztoolkit.basicOptions.debug.disableDebugBridgePassword =
addon.data.env === "development";
Zotero[config.addonInstance] = addon;
// Trigger addon hook for initialization
addon.hooks.onStartup();
}

View File

@ -1,15 +0,0 @@
/*
* This file defines the modules' base class.
*/
import BetterNotes from "./addon";
class AddonBase {
protected _Addon: BetterNotes;
constructor(parent: BetterNotes) {
this._Addon = parent;
}
}
export default AddonBase;

1104
src/modules/convert/api.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
import { getPref } from "../../utils/prefs";
export function initEditorImagePreviewer(editor: Zotero.EditorInstance) {
const openPreview = (e: MouseEvent) => {
const imgs = editor._iframeWindow.document
.querySelector(".primary-editor")
?.querySelectorAll("img");
if (!imgs) {
return;
}
const imageList = Array.from(imgs);
addon.api.window.showImageViewer(
imageList.map((elem) => elem.src),
imageList.indexOf(e.target as HTMLImageElement),
editor._item.getNoteTitle()
);
};
editor._iframeWindow.document.addEventListener("dblclick", (e) => {
if ((e.target as HTMLElement).tagName === "IMG") {
openPreview(e);
}
});
editor._iframeWindow.document.addEventListener("click", (e) => {
if ((e.target as HTMLElement).tagName === "IMG" && e.ctrlKey) {
openPreview(e);
}
});
}

View File

@ -0,0 +1,34 @@
import { initEditorImagePreviewer } from "./image";
import { injectEditorCSS, injectEditorScripts } from "./inject";
import { initEditorPopup } from "./popup";
import { initEditorToolbar } from "./toolbar";
export function registerEditorInstanceHook() {
Zotero.Notes.registerEditorInstance = new Proxy(
Zotero.Notes.registerEditorInstance,
{
apply: (
target,
thisArg,
argumentsList: [instance: Zotero.EditorInstance]
) => {
target.apply(thisArg, argumentsList);
argumentsList.forEach(onEditorInstanceCreated);
},
}
);
}
async function onEditorInstanceCreated(editor: Zotero.EditorInstance) {
await editor._initPromise;
// item.getNote may not be initialized yet
if (Zotero.ItemTypes.getID("note") !== editor._item.itemTypeID) {
return;
}
await injectEditorScripts(editor._iframeWindow);
injectEditorCSS(editor._iframeWindow);
initEditorImagePreviewer(editor);
await initEditorToolbar(editor);
initEditorPopup(editor);
}

View File

@ -0,0 +1,65 @@
import { getFileContent } from "../../utils/str";
export async function injectEditorScripts(win: Window) {
ztoolkit.UI.appendElement(
{
tag: "script",
id: "betternotes-script",
properties: {
innerHTML: await getFileContent(
rootURI + "chrome/content/scripts/editorScript.js"
),
},
ignoreIfExists: true,
},
win.document.head
);
}
export function injectEditorCSS(win: Window) {
ztoolkit.UI.appendElement(
{
tag: "style",
id: "betternotes-style",
properties: {
innerHTML: `
.primary-editor > h1::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3E%E6%9C%AA%E6%A0%87%E9%A2%98-1%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12.29%2C16.8H11.14V12.33H6.07V16.8H4.92V7H6.07v4.3h5.07V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M18.05%2C16.8H16.93V8.41a4%2C4%2C0%2C0%2C1-.9.53%2C6.52%2C6.52%2C0%2C0%2C1-1.14.44l-.32-1a8.2%2C8.2%2C0%2C0%2C0%2C1.67-.67%2C6.31%2C6.31%2C0%2C0%2C0%2C1.39-1h.42Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > h2::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22a%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M14.14%2C16.8v-.48a4.1%2C4.1%2C0%2C0%2C1%2C.14-1.11%2C2.86%2C2.86%2C0%2C0%2C1%2C.45-.91%2C5.49%2C5.49%2C0%2C0%2C1%2C.83-.86c.33-.29.75-.61%2C1.24-1a7.43%2C7.43%2C0%2C0%2C0%2C.9-.73%2C3.9%2C3.9%2C0%2C0%2C0%2C.57-.7%2C2.22%2C2.22%2C0%2C0%2C0%2C.3-.66%2C2.87%2C2.87%2C0%2C0%2C0%2C.11-.77%2C1.89%2C1.89%2C0%2C0%2C0-.47-1.32%2C1.66%2C1.66%2C0%2C0%2C0-1.28-.5A3.17%2C3.17%2C0%2C0%2C0%2C15.7%2C8a3.49%2C3.49%2C0%2C0%2C0-1.08.76l-.68-.65a4.26%2C4.26%2C0%2C0%2C1%2C1.39-1A4%2C4%2C0%2C0%2C1%2C17%2C6.84a2.62%2C2.62%2C0%2C0%2C1%2C2.83%2C2.67%2C3.58%2C3.58%2C0%2C0%2C1-.15%2C1%2C3.09%2C3.09%2C0%2C0%2C1-.41.9%2C5.53%2C5.53%2C0%2C0%2C1-.67.81%2C9%2C9%2C0%2C0%2C1-.95.79c-.46.32-.84.59-1.13.82a4.68%2C4.68%2C0%2C0%2C0-.71.64%2C2%2C2%2C0%2C0%2C0-.38.6%2C2.08%2C2.08%2C0%2C0%2C0-.11.69h4.88v1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > h3::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16.14l.51-.8a4.75%2C4.75%2C0%2C0%2C0%2C1.1.52%2C4.27%2C4.27%2C0%2C0%2C0%2C1.12.16%2C2.29%2C2.29%2C0%2C0%2C0%2C1.64-.52A1.77%2C1.77%2C0%2C0%2C0%2C19%2C14.17a1.7%2C1.7%2C0%2C0%2C0-.68-1.48%2C3.6%2C3.6%2C0%2C0%2C0-2.06-.48H15.4v-1h.77A3%2C3%2C0%2C0%2C0%2C18%2C10.81a1.65%2C1.65%2C0%2C0%2C0%2C.6-1.41%2C1.47%2C1.47%2C0%2C0%2C0-.47-1.19A1.67%2C1.67%2C0%2C0%2C0%2C17%2C7.79a3.33%2C3.33%2C0%2C0%2C0-2.08.73l-.59-.75a4.4%2C4.4%2C0%2C0%2C1%2C1.28-.71A4.35%2C4.35%2C0%2C0%2C1%2C17%2C6.84a2.84%2C2.84%2C0%2C0%2C1%2C2%2C.65%2C2.21%2C2.21%2C0%2C0%2C1%2C.74%2C1.78%2C2.35%2C2.35%2C0%2C0%2C1-.49%2C1.5%2C2.7%2C2.7%2C0%2C0%2C1-1.46.89v0a2.74%2C2.74%2C0%2C0%2C1%2C1.65.74%2C2.15%2C2.15%2C0%2C0%2C1%2C.66%2C1.65%2C2.64%2C2.64%2C0%2C0%2C1-.9%2C2.12%2C3.44%2C3.44%2C0%2C0%2C1-2.34.78%2C5.3%2C5.3%2C0%2C0%2C1-1.48-.2A5%2C5%2C0%2C0%2C1%2C14%2C16.14Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > h4::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M19.43%2C6.92v6.59h1.05v1.05H19.43V16.9H18.31V14.56H13.66v-1c.43-.49.87-1%2C1.31-1.57s.87-1.13%2C1.27-1.7S17%2C9.14%2C17.36%2C8.57a16.51%2C16.51%2C0%2C0%2C0%2C.86-1.65Zm-4.49%2C6.59h3.37V8.63c-.34.61-.67%2C1.15-1%2C1.63s-.6.91-.87%2C1.3-.56.74-.81%2C1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > h5::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16l.58-.76a3.67%2C3.67%2C0%2C0%2C0%2C1%2C.58A3.44%2C3.44%2C0%2C0%2C0%2C16.8%2C16a2.17%2C2.17%2C0%2C0%2C0%2C1.58-.6A2%2C2%2C0%2C0%2C0%2C19%2C13.88a1.85%2C1.85%2C0%2C0%2C0-.64-1.5%2C2.83%2C2.83%2C0%2C0%2C0-1.86-.54c-.27%2C0-.55%2C0-.86%2C0s-.58%2C0-.81.06L15.17%2C7H19.7V8H16.14l-.2%2C2.88.47%2C0h.43a3.5%2C3.5%2C0%2C0%2C1%2C2.43.79%2C2.74%2C2.74%2C0%2C0%2C1%2C.88%2C2.16%2C3%2C3%2C0%2C0%2C1-.94%2C2.3%2C3.41%2C3.41%2C0%2C0%2C1-2.4.87%2C4.45%2C4.45%2C0%2C0%2C1-1.5-.24A4.81%2C4.81%2C0%2C0%2C1%2C14%2C16Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > h6::before {
margin-left: -64px !important;
padding-left: 40px !important;
content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M20.18%2C13.7a3.24%2C3.24%2C0%2C0%2C1-.88%2C2.38%2C2.94%2C2.94%2C0%2C0%2C1-2.2.9%2C2.69%2C2.69%2C0%2C0%2C1-2.31-1.17A5.59%2C5.59%2C0%2C0%2C1%2C14%2C12.49a12.18%2C12.18%2C0%2C0%2C1%2C.2-2.14%2C5.16%2C5.16%2C0%2C0%2C1%2C.84-2A3.65%2C3.65%2C0%2C0%2C1%2C16.27%2C7.2%2C3.71%2C3.71%2C0%2C0%2C1%2C18%2C6.84%2C3.14%2C3.14%2C0%2C0%2C1%2C19%2C7a3.59%2C3.59%2C0%2C0%2C1%2C1%2C.5l-.56.77a2.3%2C2.3%2C0%2C0%2C0-1.49-.48A2.3%2C2.3%2C0%2C0%2C0%2C16.79%2C8a3%2C3%2C0%2C0%2C0-.92.85%2C3.79%2C3.79%2C0%2C0%2C0-.56%2C1.25%2C6.56%2C6.56%2C0%2C0%2C0-.19%2C1.65h0a2.61%2C2.61%2C0%2C0%2C1%2C1-.84%2C2.91%2C2.91%2C0%2C0%2C1%2C1.23-.28%2C2.63%2C2.63%2C0%2C0%2C1%2C2%2C.85A3.09%2C3.09%2C0%2C0%2C1%2C20.18%2C13.7ZM19%2C13.78a2.28%2C2.28%2C0%2C0%2C0-.5-1.62%2C1.67%2C1.67%2C0%2C0%2C0-1.29-.54%2C2%2C2%2C0%2C0%2C0-1.5.58%2C2%2C2%2C0%2C0%2C0-.56%2C1.4%2C2.65%2C2.65%2C0%2C0%2C0%2C.55%2C1.74%2C1.85%2C1.85%2C0%2C0%2C0%2C2.78.1A2.38%2C2.38%2C0%2C0%2C0%2C19%2C13.78Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
}
.primary-editor > p, .primary-editor h1, .primary-editor h2, .primary-editor h3, .primary-editor h4, .primary-editor h5, .primary-editor h6, .primary-editor pre, .primary-editor blockquote, .primary-editor table, .primary-editor ul, .primary-editor ol, .primary-editor hr{
max-width: unset
}
`,
},
ignoreIfExists: true,
},
win.document.head
);
}

257
src/modules/editor/popup.ts Normal file
View File

@ -0,0 +1,257 @@
import { ICONS } from "../../utils/config";
import {
del,
getEditorCore,
getLineAtCursor,
getPositionAtLine,
getURLAtCursor,
insert,
updateImageDimensionsAtCursor,
updateURLAtCursor,
} from "../../utils/editor";
import { getNoteLink, getNoteLinkParams } from "../../utils/link";
import { getString } from "../../utils/locale";
export function initEditorPopup(editor: Zotero.EditorInstance) {
const ob = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
for (const mut of muts) {
ztoolkit.log(mut);
if (
(mut.addedNodes.length &&
(mut.addedNodes[0] as HTMLElement).querySelector(".link-popup")) ||
(mut.attributeName === "href" &&
mut.target.parentElement?.classList.contains("link"))
) {
updateEditorLinkPopup(editor);
} else if (
mut.addedNodes.length &&
(mut.addedNodes[0] as HTMLElement).querySelector(".image-popup")
) {
updateEditorImagePopup(editor);
}
}
});
ob.observe(
editor._iframeWindow.document.querySelector(".relative-container")!,
{
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["href"],
}
);
}
async function updateEditorLinkPopup(editor: Zotero.EditorInstance) {
const _window = editor._iframeWindow;
const link = getURLAtCursor(editor);
const linkParams = getNoteLinkParams(link);
const linkNote = linkParams.noteItem;
const editorNote = editor._item;
// If the note is invalid, we remove the buttons
if (linkNote) {
const insertButton = ztoolkit.UI.createElement(_window.document, "button", {
id: "link-popup-insert",
properties: {
title: `Import Linked Note: ${linkNote.getNoteTitle()}`,
innerHTML: ICONS["embedLinkContent"],
},
classList: ["link-popup-extra"],
removeIfExists: true,
listeners: [
{
type: "click",
listener: async (e) => {
if (!linkParams.ignore) {
const templateText = await addon.api.template.runTemplate(
"[QuickImportV2]",
"link, noteItem",
[link, editorNote]
);
// auto insert to anchor position
updateURLAtCursor(
editor,
undefined,
getNoteLink(
linkNote,
Object.assign({}, linkParams, { ignore: true })
)!
);
insert(editor, templateText);
} else {
updateURLAtCursor(
editor,
undefined,
getNoteLink(
linkNote,
Object.assign({}, linkParams, { ignore: null })
)!
);
const lineIndex = getLineAtCursor(editor);
del(
editor,
getPositionAtLine(editor, lineIndex),
getPositionAtLine(editor, lineIndex + 1)
);
}
},
},
],
});
const updateButton = ztoolkit.UI.createElement(_window.document, "button", {
id: "link-popup-update",
properties: {
title: `Update Link Text: ${linkNote.getNoteTitle()}`,
innerHTML: ICONS["updateLinkText"],
},
classList: ["link-popup-extra"],
removeIfExists: true,
listeners: [
{
type: "click",
listener: async (e) => {
updateURLAtCursor(
editor,
linkNote.getNoteTitle(),
getURLAtCursor(editor)
);
},
},
],
});
const openButton = ztoolkit.UI.createElement(_window.document, "button", {
id: "link-popup-open",
properties: {
title: "Open in new window",
innerHTML: ICONS["openInNewWindow"],
},
classList: ["link-popup-extra"],
removeIfExists: true,
listeners: [
{
type: "click",
listener: async (e) => {
ZoteroPane.openNoteWindow(linkNote.id);
},
},
],
});
_window.document
.querySelector(".link-popup")
?.append(insertButton, updateButton, openButton);
// if (linkPopup) {
// if (Zotero.Prefs.get("Knowledge4Zotero.linkAction.preview") as boolean) {
// let previewContainer =
// _window.document.getElementById("note-link-preview");
// if (previewContainer) {
// previewContainer.remove();
// }
// previewContainer = ztoolkit.UI.createElement(
// _window.document,
// "div"
// ) as HTMLDivElement;
// previewContainer.id = "note-link-preview";
// previewContainer.className = "ProseMirror primary-editor";
// previewContainer.innerHTML =
// await this._Addon.NoteParse.parseNoteStyleHTML(linkNote);
// previewContainer.addEventListener("click", (e) => {
// this._Addon.WorkspaceWindow.setWorkspaceNote("preview", linkNote);
// });
// linkPopup.append(previewContainer);
// previewContainer.setAttribute(
// "style",
// `width: 98%;height: ${
// linkPopup ? Math.min(linkPopup.offsetTop, 300) : 300
// }px;position: absolute;background: white;bottom: 36px;overflow: hidden;box-shadow: 0 0 5px 5px rgba(0,0,0,0.2);border-radius: 5px;cursor: pointer;opacity: 0.9;`
// );
// previewContainer
// .querySelector("div[data-schema-version]")
// ?.childNodes.forEach((node) => {
// if ((node as Element).setAttribute) {
// (node as Element).setAttribute("style", "margin: 0");
// } else {
// node.remove();
// }
// });
// }
// }
} else {
Array.from(_window.document.querySelectorAll(".link-popup-extra")).forEach(
(elem) => elem.remove()
);
}
}
function updateEditorImagePopup(editor: Zotero.EditorInstance) {
ztoolkit.UI.appendElement(
{
tag: "fragment",
children: [
{
tag: "button",
id: "image-popup-preview",
properties: {
innerHTML: ICONS.previewImage,
title: getString("editor.previewImage.title"),
},
removeIfExists: true,
listeners: [
{
type: "click",
listener: (e) => {
const imgs = editor._iframeWindow.document
.querySelector(".primary-editor")
?.querySelectorAll("img");
if (!imgs) {
return;
}
const imageList = Array.from(imgs);
addon.api.window.showImageViewer(
imageList.map((elem) => elem.src),
imageList.indexOf(
editor._iframeWindow.document
.querySelector(".primary-editor")
?.querySelector(".selected")
?.querySelector("img") as HTMLImageElement
),
editor._item.getNoteTitle()
);
},
},
],
},
{
tag: "button",
id: "image-popup-resize",
properties: {
innerHTML: ICONS.resizeImage,
title: getString("editor.resizeImage.title"),
},
removeIfExists: true,
listeners: [
{
type: "click",
listener: (e) => {
const newWidth = parseFloat(
editor._iframeWindow.prompt(
getString("editor.resizeImage.prompt"),
// @ts-ignore
getEditorCore(editor).view.state.selection.node?.attrs
?.width
) || ""
);
if (newWidth && newWidth > 10) {
updateImageDimensionsAtCursor(editor, newWidth);
}
},
},
],
},
],
},
editor._iframeWindow.document.querySelector(".image-popup")!
);
}

View File

@ -0,0 +1,405 @@
import { config } from "../../../package.json";
import { ICONS } from "../../utils/config";
import { getLineAtCursor } from "../../utils/editor";
import { showHint } from "../../utils/hint";
import { getNoteLink, getNoteLinkParams } from "../../utils/link";
import { getString } from "../../utils/locale";
import {
addLineToNote,
getNoteTreeFlattened,
getNoteType,
} from "../../utils/note";
import { getPref } from "../../utils/prefs";
import { slice } from "../../utils/str";
export async function initEditorToolbar(editor: Zotero.EditorInstance) {
const noteItem = editor._item;
const noteType = getNoteType(noteItem.id);
const toolbar = await registerEditorToolbar(editor, makeId("toolbar"));
// Settings
const settingsButton = await registerEditorToolbarDropdown(
editor,
toolbar,
makeId("settings"),
ICONS.settings,
getString("editor.toolbar.settings.title"),
"start",
(e) => {}
);
settingsButton.addEventListener("mouseenter", (ev) => {
const settingsMenu: PopupData[] = [
{
id: makeId("settings-openWorkspace"),
text: getString("editor.toolbar.settings.openWorkspace"),
callback: (e) => {
addon.hooks.onOpenWorkspace("tab");
},
},
{
id: makeId("settings-setWorkspace"),
text: getString("editor.toolbar.settings.setWorkspace"),
callback: (e) => {
addon.hooks.onSetWorkspaceNote(e.editor._item.id, "main");
},
},
{
id: makeId("settings-insertTemplate"),
text: getString("editor.toolbar.settings.insertTemplate"),
callback: (e) => {
addon.api.window.showTemplatePicker(
e.editor._item.id,
getLineAtCursor(e.editor)
);
},
},
{
id: makeId("settings-copyLink"),
text: getString("editor.toolbar.settings.copyLink"),
callback: (e) => {
const link =
getNoteLink(e.editor._item, {
lineIndex: getLineAtCursor(e.editor),
}) || "";
new ztoolkit.Clipboard()
.addText(link, "text/unicode")
.addText(
`<a href="${link}">${
e.editor._item.getNoteTitle().trim() || link
}</a>`,
"text/html"
)
.copy();
showHint(`Link ${link} copied`);
},
},
];
const settingsPopup = registerEditorToolbarPopup(
editor,
settingsButton,
`${config.addonRef}-settings-popup`,
"left",
settingsMenu
);
});
settingsButton.addEventListener("mouseleave", (ev) => {
editor._iframeWindow.document
.querySelector(`#${makeId("settings-popup")}`)
?.remove();
});
settingsButton.addEventListener("click", (ev) => {
editor._iframeWindow.document
.querySelector(`#${makeId("settings-popup")}`)
?.remove();
});
// Center button
if (noteType === "main") {
registerEditorToolbarElement(
editor,
toolbar,
"middle",
ztoolkit.UI.createElement(editor._iframeWindow.document, "div", {
properties: { innerHTML: getString("editor.toolbar.main") },
})
);
} else {
const onTriggerMenu = (ev: MouseEvent) => {
editor._iframeWindow.focus();
const linkMenu: PopupData[] = getLinkMenuData(editor);
editor._iframeWindow.document
.querySelector(`#${makeId("link")}`)!
.querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter;
const popup = registerEditorToolbarPopup(
editor,
linkButton,
`${config.addonRef}-link-popup`,
"middle",
linkMenu
);
};
const onExitMenu = (ev: MouseEvent) => {
editor._iframeWindow.document
.querySelector(`#${makeId("link-popup")}`)
?.remove();
editor._iframeWindow.document
.querySelector(`#${makeId("link")}`)!
.querySelector(".toolbar-button")!.innerHTML = ICONS.addon;
};
const onClickMenu = async (ev: MouseEvent) => {
const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
if (!mainNote?.isNote()) {
return;
}
const lineIndex = parseInt(
(ev.target as HTMLDivElement).id.split("-").pop() || "-1"
);
const forwardLink = getNoteLink(noteItem);
const backLink = getNoteLink(mainNote, { ignore: true, lineIndex });
addLineToNote(
mainNote,
await addon.api.template.runTemplate(
"[QuickInsertV2]",
"link, linkText, subNoteItem, noteItem",
[
forwardLink,
noteItem.getNoteTitle().trim() || forwardLink,
noteItem,
mainNote,
]
),
lineIndex
);
addLineToNote(
noteItem,
await addon.api.template.runTemplate(
"[QuickBackLinkV2]",
"link, linkText, subNoteItem, noteItem",
[
backLink,
mainNote.getNoteTitle().trim() || "Workspace Note",
noteItem,
mainNote,
"",
]
)
);
onExitMenu(ev);
ev.stopPropagation();
};
const linkButton = await registerEditorToolbarDropdown(
editor,
toolbar,
makeId("link"),
ICONS.addon,
getString("editor.toolbar.link.title"),
"middle",
onClickMenu
);
linkButton.addEventListener("mouseenter", onTriggerMenu);
linkButton.addEventListener("mouseleave", onExitMenu);
linkButton.addEventListener("mouseleave", onExitMenu);
linkButton.addEventListener("click", (ev) => {
if ((ev.target as HTMLElement).classList.contains("option")) {
onClickMenu(ev);
}
});
}
// Export
const exportButton = await registerEditorToolbarDropdown(
editor,
toolbar,
makeId("export"),
ICONS.export,
getString("editor.toolbar.export.title"),
"end",
(e) => {
if (addon.api.sync.isSyncNote(noteItem.id)) {
addon.api.window.showSyncInfo(noteItem.id);
} else {
addon.api.window.showExportNoteOptions([noteItem.id]);
}
}
);
}
function getLinkMenuData(editor: Zotero.EditorInstance): PopupData[] {
const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
const currentNote = editor._item;
if (!workspaceNote?.isNote()) {
return [
{
id: makeId("link-popup-nodata"),
text: getString("editor.toolbar.link.popup.nodata"),
},
];
}
const nodes = getNoteTreeFlattened(workspaceNote, {
keepLink: true,
});
const menuData: PopupData[] = [];
for (const node of nodes) {
if (node.model.level === 7) {
const lastMenu =
menuData.length > 0 ? menuData[menuData.length - 1] : null;
const linkNote = getNoteLinkParams(node.model.link).noteItem;
if (linkNote && linkNote.id === currentNote.id && lastMenu) {
lastMenu.suffix = "🔗";
}
continue;
}
menuData.push({
id: makeId(
`link-popup-${
getPref("editor.link.insertPosition")
? node.model.lineIndex - 1
: node.model.endIndex
}`
),
text: node.model.name,
prefix: "·".repeat(node.model.level - 1),
});
}
return menuData;
}
async function registerEditorToolbar(
editor: Zotero.EditorInstance,
id: string
) {
await editor._initPromise;
const _document = editor._iframeWindow.document;
const toolbar = ztoolkit.UI.createElement(_document, "div", {
attributes: {
id,
},
classList: ["toolbar"],
children: [
{
tag: "div",
classList: ["start"],
},
{
tag: "div",
classList: ["middle"],
},
{
tag: "div",
classList: ["end"],
},
],
ignoreIfExists: true,
}) as HTMLDivElement;
_document.querySelector(".editor")?.childNodes[0].before(toolbar);
return toolbar;
}
async function registerEditorToolbarDropdown(
editor: Zotero.EditorInstance,
toolbar: HTMLDivElement,
id: string,
icon: string,
title: string,
position: "start" | "middle" | "end",
callback: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any
) {
await editor._initPromise;
const _document = editor._iframeWindow.document;
const dropdown = ztoolkit.UI.createElement(_document, "div", {
attributes: {
id,
title,
},
classList: ["dropdown", "more-dropdown"],
children: [
{
tag: "button",
attributes: {
title,
},
properties: {
innerHTML: icon,
},
classList: ["toolbar-button"],
listeners: [
{
type: "click",
listener: (e) => {
Object.assign(e, { editor });
if (callback) {
callback(
e as any as MouseEvent & { editor: Zotero.EditorInstance }
);
}
},
},
],
},
],
skipIfExists: true,
});
toolbar.querySelector(`.${position}`)?.append(dropdown);
return dropdown;
}
declare interface PopupData {
id: string;
text: string;
prefix?: string;
suffix?: string;
callback?: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any;
}
async function registerEditorToolbarPopup(
editor: Zotero.EditorInstance,
dropdown: HTMLDivElement,
id: string,
align: "middle" | "left" | "right",
popupLines: PopupData[]
) {
await editor._initPromise;
const popup = ztoolkit.UI.appendElement(
{
tag: "div",
classList: ["popup"],
id,
children: popupLines.map((props) => ({
tag: "button",
classList: ["option"],
properties: {
id: props.id,
innerHTML:
slice((props.prefix || "") + props.text, 30) + (props.suffix || ""),
title: "",
},
listeners: [
{
type: "click",
listener: (e) => {
Object.assign(e, { editor });
props.callback &&
props.callback(
e as any as MouseEvent & { editor: Zotero.EditorInstance }
);
},
},
],
})),
removeIfExists: true,
},
dropdown
) as HTMLDivElement;
let style: string = "";
if (align === "middle") {
style = `right: -${popup.offsetWidth / 2 - 15}px;`;
} else if (align === "left") {
style = "left: 0; right: auto;";
} else if (align === "right") {
style = "right: 0;";
}
popup.setAttribute("style", style);
return popup;
}
async function registerEditorToolbarElement(
editor: Zotero.EditorInstance,
toolbar: HTMLDivElement,
position: "start" | "middle" | "end",
elem: HTMLElement
) {
await editor._initPromise;
toolbar.querySelector(`.${position}`)?.append(elem);
return elem;
}
function makeId(key: string) {
return `${config.addonRef}-${key}`;
}

218
src/modules/export/api.ts Normal file
View File

@ -0,0 +1,218 @@
import {
getLinkedNotesRecursively,
getNoteLink,
getNoteLinkParams,
} from "../../utils/link";
import { getString } from "../../utils/locale";
import { getLinesInNote } from "../../utils/note";
import { formatPath } from "../../utils/str";
export { exportNotes };
async function exportNotes(
noteItems: Zotero.Item[],
options: {
embedLink?: boolean;
standaloneLink?: boolean;
exportNote?: boolean;
exportMD?: boolean;
setAutoSync?: boolean;
syncDir?: string;
withYAMLHeader?: boolean;
exportDocx?: boolean;
exportPDF?: boolean;
exportFreeMind?: boolean;
}
) {
let inputNoteItems = noteItems;
// If embedLink or exportNote, create a new note item
if ((options.embedLink || options.exportNote) && !options.setAutoSync) {
inputNoteItems = [];
for (const noteItem of noteItems) {
const noteID = await ZoteroPane.newNote();
const newNote = Zotero.Items.get(noteID);
newNote.setNote(noteItem.getNote());
await newNote.saveTx({
skipSelect: true,
skipNotifier: true,
skipSyncedUpdate: true,
});
await Zotero.DB.executeTransaction(async () => {
await Zotero.Notes.copyEmbeddedImages(noteItem, newNote);
});
if (options.embedLink) {
newNote.setNote(await embedLinkedNotes(newNote));
}
await newNote.saveTx();
inputNoteItems.push(newNote);
}
}
let linkedNoteItems = [] as Zotero.Item[];
if (options.standaloneLink) {
const linkedNoteIds = [] as number[];
for (const noteItem of inputNoteItems) {
let linkedIds: number[] = getLinkedNotesRecursively(
getNoteLink(noteItem) || "",
linkedNoteIds
);
linkedNoteIds.push(...linkedIds);
}
const targetNoteItemIds = inputNoteItems.map((item) => item.id);
linkedNoteItems = Zotero.Items.get(
linkedNoteIds.filter((id) => !targetNoteItemIds.includes(id))
);
}
const allNoteItems = Array.from(
new Set(inputNoteItems.concat(linkedNoteItems))
);
if (options.exportMD) {
if (options.setAutoSync) {
const raw = await new ztoolkit.FilePicker(
`${getString("fileInterface.sync")} MarkDown File`,
"folder"
).open();
if (raw) {
const syncDir = formatPath(raw);
// Hard reset sync status for input notes
for (const noteItem of inputNoteItems) {
await toSync(noteItem, syncDir, true);
}
// Find linked notes that are not synced and include them in sync
for (const noteItem of linkedNoteItems) {
await toSync(noteItem, syncDir, false);
}
await addon.api.sync.doSync(allNoteItems, {
quiet: true,
skipActive: false,
reason: "export",
});
}
} else {
for (const noteItem of allNoteItems) {
await toMD(noteItem, {
withYAMLHeader: options.withYAMLHeader,
keepNoteLink: true,
});
}
}
}
if (options.exportDocx) {
for (const noteItem of allNoteItems) {
await toDocx(noteItem);
}
}
if (options.exportFreeMind) {
for (const noteItem of allNoteItems) {
await toFreeMind(noteItem);
}
}
if (options.embedLink && !options.exportNote) {
// If not exportNote, delete temp notes
for (const noteItem of allNoteItems) {
const _w: Window = ZoteroPane.findNoteWindow(noteItem.id);
if (_w) {
_w.close();
}
await Zotero.Items.erase(noteItem.id);
}
} else if (options.exportPDF) {
for (const noteItem of allNoteItems) {
await addon.api._export.savePDF(noteItem.id);
}
} else if (options.exportNote) {
for (const noteItem of allNoteItems) {
ZoteroPane.openNoteWindow(noteItem.id);
}
}
}
async function toMD(
noteItem: Zotero.Item,
options: {
filename?: string;
keepNoteLink?: boolean;
withYAMLHeader?: boolean;
} = {}
) {
let filename = options.filename;
if (!filename) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} MarkDown File`,
"save",
[["MarkDown File(*.md)", "*.md"]],
`${noteItem.getNoteTitle()}.md`
).open();
if (!raw) return;
filename = formatPath(raw, ".md");
}
await addon.api._export.saveMD(filename, noteItem.id, options);
}
async function toSync(
noteItem: Zotero.Item,
syncDir: string,
overwrite: boolean = false
) {
if (!overwrite && addon.api.sync.isSyncNote(noteItem.id)) {
return;
}
addon.api.sync.updateSyncStatus(noteItem.id, {
path: syncDir,
filename: await addon.api.sync.getMDFileName(noteItem.id, syncDir),
md5: "",
noteMd5: Zotero.Utilities.Internal.md5(noteItem.getNote(), false),
lastsync: 0,
itemID: noteItem.id,
});
}
async function toDocx(noteItem: Zotero.Item) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} MS Word Docx`,
"save",
[["MS Word Docx File(*.docx)", "*.docx"]],
`${noteItem.getNoteTitle()}.docx`
).open();
if (!raw) return;
const filename = formatPath(raw, ".docx");
await addon.api._export.saveDocx(filename, noteItem.id);
}
async function toFreeMind(noteItem: Zotero.Item) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} FreeMind XML`,
"save",
[["FreeMind XML File(*.mm)", "*.mm"]],
`${noteItem.getNoteTitle()}.mm`
).open();
if (!raw) return;
const filename = formatPath(raw, ".mm");
await addon.api._export.saveFreeMind(filename, noteItem.id);
}
async function embedLinkedNotes(noteItem: Zotero.Item): Promise<string> {
const parser = ztoolkit.getDOMParser();
let newLines: string[] = [];
const noteLines = getLinesInNote(noteItem);
for (let i in noteLines) {
newLines.push(noteLines[i]);
const doc = parser.parseFromString(noteLines[i], "text/html");
const linkParams = Array.from(doc.querySelectorAll("a"))
.filter((a) => a.href.startsWith("zotero://note/"))
.map((a) => getNoteLinkParams(a.href))
.filter((p) => p.noteItem && !p.ignore);
for (const linkParam of linkParams) {
const html = await addon.api.template.runTemplate(
"[QuickImportV2]",
"link, noteItem",
[linkParam.link, noteItem]
);
newLines.push(html);
}
}
return newLines.join("\n");
}

View File

@ -0,0 +1,68 @@
import { showHintWithLink } from "../../utils/hint";
import { renderNoteHTML } from "../../utils/note";
import { getFileContent, randomString } from "../../utils/str";
import { waitUtilAsync } from "../../utils/wait";
export async function saveDocx(filename: string, noteId: number) {
const noteItem = Zotero.Items.get(noteId);
await Zotero.File.putContentsAsync(filename, await note2docx(noteItem));
showHintWithLink(`Note Saved to ${filename}`, "Show in Folder", (ev) => {
Zotero.File.reveal(filename);
});
}
async function note2docx(noteItem: Zotero.Item) {
const renderedContent = await renderNoteHTML(noteItem);
let htmlDoc =
'<!DOCTYPE html>\n<html lang="en"><head><meta charset="UTF-8"></head>\n';
htmlDoc += renderedContent;
htmlDoc += "\n</html>";
let blob: ArrayBufferLike;
const lock = Zotero.Promise.defer();
const jobId = randomString(6, new Date().toUTCString());
const listener = (ev: MessageEvent) => {
if (ev.data.type === "parseDocxReturn" && ev.data.jobId === jobId) {
blob = ev.data.message;
lock.resolve();
}
};
const worker = await getWorker();
worker.contentWindow?.addEventListener("message", listener);
worker.contentWindow?.postMessage(
{
type: "parseDocx",
jobId,
message: htmlDoc,
},
"*"
);
await lock.promise;
worker.contentWindow?.removeEventListener("message", listener);
return blob!;
}
async function getWorker() {
if (addon.data.export.docx.worker) {
return addon.data.export.docx.worker;
}
const worker = Zotero.Browser.createHiddenBrowser(
window
) as HTMLIFrameElement;
await waitUtilAsync(() => worker.contentDocument?.readyState === "complete");
const doc = worker.contentDocument;
ztoolkit.UI.appendElement(
{
tag: "script",
properties: {
innerHTML: await getFileContent(
rootURI + "chrome/content/scripts/docxWorker.js"
),
},
},
doc?.head!
);
addon.data.export.docx.worker = worker;
return worker;
}

View File

@ -0,0 +1,227 @@
import { getString } from "../../utils/locale";
import { getPref, setPref } from "../../utils/prefs";
import { fill, slice } from "../../utils/str";
enum OPTIONS {
"embedLink",
"standaloneLink",
"keepLink",
"exportMD",
"setAutoSync",
"withYAMLHeader",
"exportDocx",
"exportPDF",
"exportFreeMind",
"exportNote",
}
export async function showExportNoteOptions(noteIds: number[]) {
const items = Zotero.Items.get(noteIds);
const noteItems: Zotero.Item[] = [];
items.forEach((item) => {
if (item.isNote()) {
noteItems.push(item);
}
if (item.isRegularItem()) {
noteItems.splice(0, 0, ...Zotero.Items.get(item.getNotes()));
}
});
if (noteItems.length === 0) {
return;
}
const dataKeys = Object.keys(OPTIONS).filter(
(value) => typeof value === "string"
);
const data = dataKeys.reduce((acc, key) => {
acc[key] = getPref(`export.${key}`) as boolean;
return acc;
}, {} as Record<string, any>);
data.loadCallback = () => {
const doc = dialog.window.document;
const standaloneLinkRadio = doc.querySelector(
"#standaloneLink"
) as HTMLInputElement;
const autoSyncRadio = doc.querySelector("#setAutoSync") as HTMLInputElement;
function updateSyncCheckbox() {
const standaloneLinkEnabled = standaloneLinkRadio.checked;
if (!standaloneLinkEnabled) {
autoSyncRadio.checked = false;
autoSyncRadio.disabled = true;
} else {
autoSyncRadio.disabled = false;
}
}
Array.from(doc.querySelectorAll('input[name="linkMode"]')).forEach((elem) =>
elem.addEventListener("change", updateSyncCheckbox)
);
updateSyncCheckbox();
};
const dialog = new ztoolkit.Dialog(17, 1)
.setDialogData(data)
.addCell(0, 0, {
tag: "div",
styles: {
display: "grid",
gridTemplateColumns: "1fr 20px",
rowGap: "10px",
columnGap: "5px",
},
children: [
{
tag: "label",
properties: {
innerHTML: `${getString("export.target")}: ${fill(
slice(noteItems[0].getNoteTitle(), 40),
40
)}${
noteItems.length > 1 ? ` and ${noteItems.length - 1} more` : ""
}`,
},
},
],
})
.addCell(1, 0, makeHeadingLine(getString("export.options.linkMode")))
.addCell(2, 0, makeRadioLine("embedLink", "linkMode"))
.addCell(3, 0, makeRadioLine("standaloneLink", "linkMode"))
.addCell(4, 0, makeRadioLine("keepLink", "linkMode"))
.addCell(5, 0, makeHeadingLine(getString("export.options.MD")))
.addCell(6, 0, makeCheckboxLine("exportMD"))
.addCell(7, 0, makeCheckboxLine("setAutoSync"))
.addCell(8, 0, makeCheckboxLine("withYAMLHeader"))
.addCell(9, 0, makeHeadingLine(getString("export.options.Docx")))
.addCell(10, 0, makeCheckboxLine("exportDocx"))
.addCell(11, 0, makeHeadingLine(getString("export.options.PDF")))
.addCell(12, 0, makeCheckboxLine("exportPDF"))
.addCell(13, 0, makeHeadingLine(getString("export.options.mm")))
.addCell(14, 0, makeCheckboxLine("exportFreeMind"))
.addCell(15, 0, makeHeadingLine(getString("export.options.note")))
.addCell(16, 0, makeCheckboxLine("exportNote"))
.addButton(getString("export.confirm"), "confirm")
.addButton(getString("export.cancel"), "cancel")
.open(getString("export.title"), {
resizable: true,
centerscreen: true,
fitContent: true,
noDialogMode: true,
});
await data.unloadLock?.promise;
if (data._lastButtonId === "confirm") {
addon.api._export.exportNotes(noteItems, data as Record<string, boolean>);
dataKeys.forEach((key) => {
setPref(`export.${key}`, Boolean(data[key]));
});
}
}
function makeHeadingLine(text: string) {
return {
tag: "div",
styles: {
display: "grid",
gridTemplateColumns: "1fr 20px",
rowGap: "10px",
columnGap: "5px",
},
children: [
{
tag: "h3",
properties: {
innerHTML: text,
},
},
],
};
}
function makeCheckboxLine(dataKey: string, callback?: (ev: Event) => void) {
return {
tag: "div",
styles: {
display: "grid",
gridTemplateColumns: "1fr 20px",
rowGap: "10px",
columnGap: "5px",
},
children: [
{
tag: "label",
attributes: {
for: dataKey,
},
properties: {
innerHTML: getString(`export.${dataKey}`),
},
},
{
tag: "input",
id: dataKey,
attributes: {
"data-bind": dataKey,
"data-prop": "checked",
},
properties: {
type: "checkbox",
},
listeners: callback
? [
{
type: "change",
listener: callback,
},
]
: [],
},
],
};
}
function makeRadioLine(
dataKey: string,
radioName: string,
callback?: (ev: Event) => void
) {
return {
tag: "div",
styles: {
display: "grid",
gridTemplateColumns: "1fr 20px",
rowGap: "10px",
columnGap: "5px",
},
children: [
{
tag: "label",
attributes: {
for: dataKey,
},
properties: {
innerHTML: getString(`export.${dataKey}`),
},
},
{
tag: "input",
id: dataKey,
attributes: {
"data-bind": dataKey,
"data-prop": "checked",
},
properties: {
type: "radio",
name: radioName,
value: dataKey,
},
listeners: callback
? [
{
type: "change",
listener: callback,
},
]
: [],
},
],
};
}

View File

@ -0,0 +1,90 @@
import TreeModel = require("tree-model");
import { showHintWithLink } from "../../utils/hint";
import { getNoteTree, parseHTMLLines, renderNoteHTML } from "../../utils/note";
export async function saveFreeMind(filename: string, noteId: number) {
const noteItem = Zotero.Items.get(noteId);
await Zotero.File.putContentsAsync(filename, await note2mm(noteItem));
showHintWithLink(`Note Saved to ${filename}`, "Show in Folder", (ev) => {
Zotero.File.reveal(filename);
});
}
async function note2mm(
noteItem: Zotero.Item,
options: { withContent?: boolean } = { withContent: true }
) {
const root = getNoteTree(noteItem, false);
const textNodeForEach = (e: Node, callbackfn: Function) => {
if (e.nodeType === document.TEXT_NODE) {
callbackfn(e);
return;
}
e.childNodes.forEach((_e) => textNodeForEach(_e, callbackfn));
};
const html2Escape = (sHtml: string) => {
return sHtml.replace(/[<>&"]/g, function (c) {
return { "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[c]!;
});
};
let lines: string[] = [];
if (options.withContent) {
const doc = ztoolkit
.getDOMParser()
.parseFromString(await renderNoteHTML(noteItem), "text/html");
textNodeForEach(doc.body, (e: Text) => {
e.data = html2Escape(e.data);
});
lines = parseHTMLLines(doc.body.innerHTML);
}
const convertClosingTags = (htmlStr: string) => {
const regConfs = [
{
reg: /<br[^>]*?>/g,
cbk: (str: string) => "<br></br>",
},
{
reg: /<img[^>]*?>/g,
cbk: (str: string) => {
return `<img ${str.match(/src="[^"]+"/g)}></img>`;
},
},
];
for (const regConf of regConfs) {
htmlStr = htmlStr.replace(regConf.reg, regConf.cbk);
}
return htmlStr;
};
const convertNode = (node: TreeModel.Node<NoteNodeData>) => {
mmXML += `<node ID="${node.model.id}" TEXT="${html2Escape(
node.model.name || noteItem.getNoteTitle()
)}"><hook NAME="AlwaysUnfoldedNode" />`;
if (
options.withContent &&
node.model.lineIndex >= 0 &&
node.model.endIndex >= 0
) {
mmXML += `<richcontent TYPE="NOTE" CONTENT-TYPE="xml/"><html><head></head><body>${convertClosingTags(
lines
.slice(
node.model.lineIndex,
node.hasChildren()
? node.children[0].model.lineIndex
: node.model.endIndex + 1
)
.join("\n")
)}</body></html></richcontent>`;
}
if (node.hasChildren()) {
node.children.forEach((child: TreeModel.Node<NoteNodeData>) => {
convertNode(child);
});
}
mmXML += "</node>";
};
let mmXML = '<map version="freeplane 1.9.0">';
convertNode(root);
mmXML += "</map>";
ztoolkit.log(mmXML);
return mmXML;
}

View File

@ -0,0 +1,60 @@
import { showHintWithLink } from "../../utils/hint";
import { formatPath } from "../../utils/str";
export async function saveMD(
filename: string,
noteId: number,
options: {
keepNoteLink?: boolean;
withYAMLHeader?: boolean;
}
) {
const noteItem = Zotero.Items.get(noteId);
const dir = OS.Path.join(
...OS.Path.split(formatPath(filename)).components.slice(0, -1)
);
const hasImage = noteItem.getNote().includes("<img");
if (hasImage) {
await Zotero.File.createDirectoryIfMissingAsync(dir);
}
await Zotero.File.putContentsAsync(
filename,
await addon.api.convert.note2md(noteItem, dir, options)
);
showHintWithLink(`Note Saved to ${filename}`, "Show in Folder", (ev) => {
Zotero.File.reveal(filename);
});
}
export async function syncMDBatch(saveDir: string, noteIds: number[]) {
const noteItems = Zotero.Items.get(noteIds);
await Zotero.File.createDirectoryIfMissingAsync(saveDir);
const attachmentsDir = formatPath(OS.Path.join(saveDir, "attachments"));
const hasImage = noteItems.some((noteItem) =>
noteItem.getNote().includes("<img")
);
if (hasImage) {
await Zotero.File.createDirectoryIfMissingAsync(attachmentsDir);
}
for (const noteItem of noteItems) {
const filename = await addon.api.sync.getMDFileName(noteItem.id, saveDir);
const filePath = OS.Path.join(saveDir, filename);
const content = await addon.api.convert.note2md(noteItem, saveDir, {
keepNoteLink: false,
withYAMLHeader: true,
});
await Zotero.File.putContentsAsync(filePath, content);
addon.api.sync.updateSyncStatus(noteItem.id, {
path: saveDir,
filename,
itemID: noteItem.id,
md5: Zotero.Utilities.Internal.md5(
addon.api.sync.getMDStatusFromContent(content).content,
false
),
noteMd5: Zotero.Utilities.Internal.md5(noteItem.getNote(), false),
lastsync: new Date().getTime(),
});
}
}

35
src/modules/export/pdf.ts Normal file
View File

@ -0,0 +1,35 @@
import { config } from "../../../package.json";
import { showHint } from "../../utils/hint";
import { renderNoteHTML } from "../../utils/note";
import { waitUtilAsync } from "../../utils/wait";
export async function savePDF(noteId: number) {
const html = await renderNoteHTML(Zotero.Items.get(noteId));
const win = window.openDialog(
`chrome://${config.addonRef}/content/pdfPrinter.html`,
`${config.addonRef}-imageViewer`,
`chrome,centerscreen,resizable,status,width=900,height=650,dialog=no`
)!;
await waitUtilAsync(() => win.document.readyState === "complete");
win.document.querySelector(".markdown-body")!.innerHTML = html;
const printPromise = Zotero.Promise.defer();
disablePrintFooterHeader();
win.addEventListener("mouseover", (ev) => {
win.close();
printPromise.resolve();
});
win.print();
await printPromise.promise;
showHint("Note Saved as PDF");
}
function disablePrintFooterHeader() {
// @ts-ignore
Zotero.Prefs.resetBranch([], "print");
Zotero.Prefs.set("print.print_footercenter", "", true);
Zotero.Prefs.set("print.print_footerleft", "", true);
Zotero.Prefs.set("print.print_footerright", "", true);
Zotero.Prefs.set("print.print_headercenter", "", true);
Zotero.Prefs.set("print.print_headerleft", "", true);
Zotero.Prefs.set("print.print_headerright", "", true);
}

293
src/modules/imageViewer.ts Normal file
View File

@ -0,0 +1,293 @@
import { config } from "../../package.json";
import { ICONS } from "../utils/config";
import { showHint, showHintWithLink } from "../utils/hint";
import { formatPath } from "../utils/str";
import { waitUtilAsync } from "../utils/wait";
export async function showImageViewer(
srcList: string[],
idx: number,
title: string
) {
if (
!addon.data.imageViewer.window ||
Components.utils.isDeadWrapper(addon.data.imageViewer.window) ||
addon.data.imageViewer.window.closed
) {
addon.data.imageViewer.window = window.openDialog(
`chrome://${config.addonRef}/content/imageViewer.html`,
`${config.addonRef}-imageViewer`,
`chrome,centerscreen,resizable,status,width=500,height=550,dialog=no${
addon.data.imageViewer.pined ? ",alwaysRaised=yes" : ""
}`
)!;
await waitUtilAsync(
() => addon.data.imageViewer.window?.document.readyState === "complete"
);
const container = addon.data.imageViewer.window.document.querySelector(
".container"
) as HTMLDivElement;
const img = addon.data.imageViewer.window.document.querySelector(
"#image"
) as HTMLImageElement;
addon.data.imageViewer.window.document
.querySelector("#left")
?.addEventListener("click", (e) => {
setIndex("left");
});
addon.data.imageViewer.window.document
.querySelector("#bigger")
?.addEventListener("click", (e) => {
addon.data.imageViewer.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
setScale(addon.data.imageViewer.scaling * 1.1);
});
addon.data.imageViewer.window.document
.querySelector("#smaller")
?.addEventListener("click", (e) => {
addon.data.imageViewer.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
setScale(addon.data.imageViewer.scaling / 1.1);
});
addon.data.imageViewer.window.document
.querySelector("#resetwidth")
?.addEventListener("click", (e) => {
setScale(1);
});
addon.data.imageViewer.window.document
.querySelector("#right")
?.addEventListener("click", (e) => {
setIndex("right");
});
addon.data.imageViewer.window.document
.querySelector("#copy")
?.addEventListener("click", (e) => {
new ztoolkit.Clipboard()
.addImage(addon.data.imageViewer.srcList[addon.data.imageViewer.idx])
.copy();
showHint("Image Copied.");
});
addon.data.imageViewer.window.document
.querySelector("#save")
?.addEventListener("click", async (e) => {
let parts =
addon.data.imageViewer.srcList[addon.data.imageViewer.idx].split(",");
if (!parts[0].includes("base64")) {
return;
}
let mime = parts[0].match(/:(.*?);/)![1];
let bstr = addon.data.imageViewer.window?.atob(parts[1])!;
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
let ext = Zotero.MIME.getPrimaryExtension(mime, "");
const filename = await new ztoolkit.FilePicker(
Zotero.getString("noteEditor.saveImageAs"),
"save",
[[`Image(*.${ext})`, `*.${ext}`]],
`${Zotero.getString("fileTypes.image").toLowerCase()}.${ext}`,
addon.data.imageViewer.window,
"images"
).open();
if (filename) {
await OS.File.writeAtomic(formatPath(filename), u8arr);
showHintWithLink(
`Image Saved to ${filename}`,
"Show in Folder",
(ev) => {
Zotero.File.reveal(filename);
}
);
}
});
addon.data.imageViewer.window.document.querySelector("#pin")!.innerHTML =
addon.data.imageViewer.pined
? ICONS.imageViewerPined
: ICONS.imageViewerPin;
addon.data.imageViewer.window.document.querySelector(
"#pin-tooltip"
)!.innerHTML = addon.data.imageViewer.pined ? "Unpin" : "Pin";
addon.data.imageViewer.window.document
.querySelector("#pin")
?.addEventListener("click", (e) => {
setPin();
});
addon.data.imageViewer.window.addEventListener("keydown", (e) => {
// ctrl+w or esc
if ((e.key === "w" && e.ctrlKey) || e.keyCode === 27) {
addon.data.imageViewer.window?.close();
}
addon.data.imageViewer.anchorPosition = {
left: img.scrollWidth / 2 - container.scrollLeft / 2,
top: img.scrollHeight / 2 - container.scrollLeft / 2,
};
if (e.keyCode === 37 || e.keyCode === 40) {
setIndex("left");
}
if (e.keyCode === 38 || e.keyCode === 39) {
setIndex("right");
}
if (e.key === "0") {
setScale(1);
} else if (e.keyCode === 107 || e.keyCode === 187 || e.key === "=") {
setScale(addon.data.imageViewer.scaling * 1.1);
} else if (e.key === "-") {
setScale(addon.data.imageViewer.scaling / 1.1);
}
});
addon.data.imageViewer.window.addEventListener("wheel", async (e) => {
addon.data.imageViewer.anchorPosition = {
left: e.pageX - container.offsetLeft,
top: e.pageY - container.offsetTop,
};
function normalizeWheelEventDirection(evt: WheelEvent) {
let delta = Math.hypot(evt.deltaX, evt.deltaY);
const angle = Math.atan2(evt.deltaY, evt.deltaX);
if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
// All that is left-up oriented has to change the sign.
delta = -delta;
}
return delta;
}
const delta = normalizeWheelEventDirection(e);
if (e.ctrlKey) {
setScale(
addon.data.imageViewer.scaling *
Math.pow(delta > 0 ? 1.1 : 1 / 1.1, Math.round(Math.abs(delta)))
);
} else if (e.shiftKey) {
container.scrollLeft -= delta * 10;
} else {
container.scrollLeft += e.deltaX * 10;
container.scrollTop += e.deltaY * 10;
}
});
img.addEventListener("mousedown", (e) => {
e.preventDefault();
// if (addon.data.imageViewer.scaling <= 1) {
// return;
// }
img.onmousemove = (e) => {
e.preventDefault();
container.scrollLeft -= e.movementX;
container.scrollTop -= e.movementY;
};
img.onmouseleave = () => {
img.onmousemove = null;
img.onmouseup = null;
};
img.onmouseup = () => {
img.onmousemove = null;
img.onmouseup = null;
};
});
}
addon.data.imageViewer.srcList = srcList;
addon.data.imageViewer.idx = idx;
addon.data.imageViewer.title = title || "Note";
setImage();
setScale(1);
addon.data.imageViewer.window.focus();
}
function setImage() {
(
addon.data.imageViewer.window?.document.querySelector(
"#image"
) as HTMLImageElement
).src = addon.data.imageViewer.srcList[addon.data.imageViewer.idx];
setTitle();
(
addon.data.imageViewer.window?.document.querySelector(
"#left-container"
) as HTMLButtonElement
).style.opacity = addon.data.imageViewer.idx === 0 ? "0.5" : "1";
(
addon.data.imageViewer.window?.document.querySelector(
"#right-container"
) as HTMLButtonElement
).style.opacity =
addon.data.imageViewer.idx === addon.data.imageViewer.srcList.length - 1
? "0.5"
: "1";
}
function setIndex(type: "left" | "right") {
if (type === "left") {
addon.data.imageViewer.idx > 0
? (addon.data.imageViewer.idx -= 1)
: undefined;
}
if (type === "right") {
addon.data.imageViewer.idx < addon.data.imageViewer.srcList.length - 1
? (addon.data.imageViewer.idx += 1)
: undefined;
}
setImage();
}
function setScale(scaling: number) {
const oldScale = addon.data.imageViewer.scaling;
addon.data.imageViewer.scaling = scaling;
if (addon.data.imageViewer.scaling > 10) {
addon.data.imageViewer.scaling = 10;
}
if (addon.data.imageViewer.scaling < 0.1) {
addon.data.imageViewer.scaling = 0.1;
}
const container = addon.data.imageViewer.window?.document.querySelector(
".container"
) as HTMLDivElement;
(
addon.data.imageViewer.window?.document.querySelector(
"#image"
) as HTMLImageElement
).style.width = `calc(100% * ${addon.data.imageViewer.scaling})`;
if (addon.data.imageViewer.scaling > 1) {
container.scrollLeft +=
addon.data.imageViewer.anchorPosition?.left! *
(addon.data.imageViewer.scaling - oldScale);
container.scrollTop +=
addon.data.imageViewer.anchorPosition?.top! *
(addon.data.imageViewer.scaling - oldScale);
}
(
addon.data.imageViewer.window?.document.querySelector(
"#bigger-container"
) as HTMLButtonElement
).style.opacity = addon.data.imageViewer.scaling === 10 ? "0.5" : "1";
(
addon.data.imageViewer.window?.document.querySelector(
"#smaller-container"
) as HTMLButtonElement
).style.opacity = addon.data.imageViewer.scaling === 0.1 ? "0.5" : "1";
// (
// addon.data.imageViewer.window.document.querySelector("#image") as HTMLImageElement
// ).style.cursor = addon.data.imageViewer.scaling <= 1 ? "default" : "move";
}
function setTitle() {
addon.data.imageViewer.window!.document.querySelector(
"title"
)!.innerText! = `${addon.data.imageViewer.idx + 1}/${
addon.data.imageViewer.srcList.length
}:${addon.data.imageViewer.title}`;
}
function setPin() {
addon.data.imageViewer.window?.close();
addon.data.imageViewer.pined = !addon.data.imageViewer.pined;
showImageViewer(
addon.data.imageViewer.srcList,
addon.data.imageViewer.idx,
addon.data.imageViewer.title
);
}

View File

@ -0,0 +1,70 @@
import { addLineToNote } from "../../utils/note";
export async function fromMD(
filepath: string,
options: {
noteId?: number;
ignoreVersion?: boolean;
append?: boolean;
appendLineIndex?: number;
} = {}
) {
let mdStatus: MDStatus;
try {
mdStatus = await addon.api.sync.getMDStatus(filepath);
} catch (e) {
ztoolkit.log(`Import Error: ${String(e)}`);
return;
}
let noteItem = options.noteId ? Zotero.Items.get(options.noteId) : undefined;
if (
!options.ignoreVersion &&
typeof mdStatus.meta?.version === "number" &&
typeof noteItem?.version === "number" &&
mdStatus.meta?.version < noteItem?.version
) {
if (
!window.confirm(
`The target note seems to be newer than the file ${filepath}. Are you sure you want to import it anyway?`
)
) {
return;
}
}
const noteStatus = noteItem
? addon.api.sync.getNoteStatus(noteItem.id)
: {
meta: '<div data-schema-version="9">',
content: "",
tail: "</div>",
};
if (!noteItem) {
noteItem = new Zotero.Item("note");
noteItem.libraryID = ZoteroPane.getSelectedLibraryID();
if (ZoteroPane.getCollectionTreeRow()?.isCollection()) {
noteItem.addToCollection(ZoteroPane.getCollectionTreeRow()?.ref.id);
}
await noteItem.saveTx({
notifierData: {
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
},
});
}
const parsedContent = await addon.api.convert.md2note(mdStatus, noteItem, {
isImport: true,
});
ztoolkit.log("import", noteStatus);
if (options.append) {
await addLineToNote(noteItem, parsedContent, options.appendLineIndex || -1);
} else {
noteItem.setNote(noteStatus!.meta + parsedContent + noteStatus!.tail);
await noteItem.saveTx({
notifierData: {
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
},
});
}
return noteItem;
}

81
src/modules/menu.ts Normal file
View File

@ -0,0 +1,81 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
export function registerMenus() {
// item
ztoolkit.Menu.register("item", { tag: "menuseparator" });
ztoolkit.Menu.register("item", {
tag: "menuitem",
label: getString("menuItem.exportNote"),
icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
commandListener: (ev) => {
addon.api.window.showExportNoteOptions(
ZoteroPane.getSelectedItems().map((item) => item.id)
);
},
});
ztoolkit.Menu.register("item", {
tag: "menuitem",
label: getString("menuItem.setMainNote"),
icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
commandListener: (ev) => {
addon.hooks.onSetWorkspaceNote(ZoteroPane.getSelectedItems()[0].id);
},
getVisibility: (elem, ev) => {
const items = ZoteroPane.getSelectedItems();
return (
items.length == 1 &&
items[0].isNote() &&
items[0].id !== addon.data.workspace.mainId
);
},
});
// menuEdit
const menuEditAnchor = document.querySelector(
"#menu_preferences"
) as XUL.MenuItem;
ztoolkit.Menu.register(
"menuEdit",
{
tag: "menuitem",
label: getString("menuEdit.templatePicker"),
icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
commandListener: (ev) => {
addon.api.window.showTemplatePicker();
},
},
"before",
menuEditAnchor
);
ztoolkit.Menu.register(
"menuEdit",
{
tag: "menuitem",
label: getString("menuEdit.templateEditor"),
icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
commandListener: (ev) => {
addon.api.window.showTemplateEditor();
},
},
"before",
menuEditAnchor
);
ztoolkit.Menu.register(
"menuEdit",
{ tag: "menuseparator" },
"before",
menuEditAnchor
);
// menuTools
ztoolkit.Menu.register("menuTools", { tag: "menuseparator" });
ztoolkit.Menu.register("menuTools", {
tag: "menuitem",
label: getString("menuTools.syncManager"),
icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
commandListener: (ev) => {
addon.api.window.showSyncManager();
},
});
}

21
src/modules/noteLink.ts Normal file
View File

@ -0,0 +1,21 @@
import { getNoteLinkParams } from "../utils/link";
export function registerNoteLinkProxyHandler() {
const openNoteExtension = {
noContent: true,
doAction: async (uri: any) => {
const linkParams = getNoteLinkParams(uri.spec);
if (linkParams.noteItem) {
addon.hooks.onOpenNote(linkParams.noteItem.id, "auto", {
lineIndex: linkParams.lineIndex || undefined,
});
}
},
newChannel: function (uri: any) {
this.doAction(uri);
},
};
Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions[
"zotero://note"
] = openNoteExtension;
}

27
src/modules/notify.ts Normal file
View 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);
}

View File

@ -0,0 +1,145 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
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
// See addon/chrome/content/preferences.xul onpaneload
if (!addon.data.prefs) {
addon.data.prefs = {
window: _window,
columns: [
{
dataKey: "title",
label: "prefs.table.title",
fixedWidth: true,
width: 100,
},
{
dataKey: "detail",
label: "prefs.table.detail",
},
],
rows: [
{
title: "Orange",
detail: "It's juicy",
},
{
title: "Banana",
detail: "It's sweet",
},
{
title: "Apple",
detail: "I mean the fruit APPLE",
},
],
};
} else {
addon.data.prefs.window = _window;
}
updatePrefsUI();
bindPrefEvents();
}
async function updatePrefsUI() {
// You can initialize some UI elements on prefs window
// with addon.data.prefs.window.document
// Or bind some events to the elements
const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer();
const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window!)
.setContainerId(`${config.addonRef}-table-container`)
.setProp({
id: `${config.addonRef}-prefs-table`,
// Do not use setLocale, as it modifies the Zotero.Intl.strings
// Set locales directly to columns
columns: addon.data.prefs?.columns.map((column) =>
Object.assign(column, {
label: getString(column.label) || column.label,
})
),
showHeader: true,
multiSelect: true,
staticColumns: true,
disableFontSizeScaling: true,
})
.setProp("getRowCount", () => addon.data.prefs?.rows.length || 0)
.setProp(
"getRowData",
(index) =>
addon.data.prefs?.rows[index] || {
title: "no data",
detail: "no data",
}
)
// Show a progress window when selection changes
.setProp("onSelectionChange", (selection) => {
new ztoolkit.ProgressWindow(config.addonName)
.createLine({
text: `Selected line: ${addon.data.prefs?.rows
.filter((v, i) => selection.isSelected(i))
.map((row) => row.title)
.join(",")}`,
progress: 100,
})
.show();
})
// When pressing delete, delete selected line and refresh table.
// Returning false to prevent default event.
.setProp("onKeyDown", (event: KeyboardEvent) => {
if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) {
addon.data.prefs!.rows =
addon.data.prefs?.rows.filter(
(v, i) => !tableHelper.treeInstance.selection.isSelected(i)
) || [];
tableHelper.render();
return false;
}
return true;
})
// For find-as-you-type
.setProp(
"getRowString",
(index) => addon.data.prefs?.rows[index].title || ""
)
// Render the table.
.render(-1, () => {
renderLock.resolve();
});
await renderLock.promise;
ztoolkit.log("Preference table rendered!");
}
function bindPrefEvents() {
addon.data
.prefs!.window.document.querySelector(
`#zotero-prefpane-${config.addonRef}-enable`
)
?.addEventListener("command", (e) => {
ztoolkit.log(e);
addon.data.prefs!.window.alert(
`Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
);
});
addon.data
.prefs!!.window.document.querySelector(
`#zotero-prefpane-${config.addonRef}-input`
)
?.addEventListener("change", (e) => {
ztoolkit.log(e);
addon.data.prefs!.window.alert(
`Successfully changed to ${(e.target as HTMLInputElement).value}!`
);
});
}

391
src/modules/reader.ts Normal file
View File

@ -0,0 +1,391 @@
import { TagElementProps } from "zotero-plugin-toolkit/dist/tools/ui";
import { config } from "../../package.json";
import { ICONS } from "../utils/config";
import { getNoteLink, getNoteLinkParams } from "../utils/link";
import { addLineToNote } from "../utils/note";
export function registerReaderInitializer() {
ztoolkit.ReaderInstance.register(
"initialized",
`${config.addonRef}-annotationButtons`,
initializeReaderAnnotationButton
);
// Force re-initialize
Zotero.Reader._readers.forEach((r) => {
initializeReaderAnnotationButton(r);
});
}
export function unregisterReaderInitializer() {
Zotero.Reader._readers.forEach((r) => {
unInitializeReaderAnnotationButton(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 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("_betternotesInitialized") === "true") {
continue;
}
moreButton.setAttribute("_betternotesInitialized", "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);
const annotationButtons: TagElementProps[] = [
{
tag: "div",
classList: ["icon"],
properties: {
innerHTML: ICONS.readerQuickNote,
},
listeners: [
{
type: "click",
listener: (e) => {
createNoteFromAnnotation(
annotationItem,
(e as MouseEvent).shiftKey ? "standalone" : "auto"
);
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,
},
];
if (annotationItem.annotationType === "image") {
annotationButtons.push({
tag: "div",
classList: ["icon"],
properties: {
innerHTML: ICONS.readerOCR,
},
listeners: [
{
type: "click",
listener: (e) => {
// TODO: OCR
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,
});
}
ztoolkit.UI.insertElementBefore(
{
tag: "fragment",
children: annotationButtons,
},
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("_betternotesInitialized") === "true") {
moreButton.removeAttribute("_betternotesInitialized");
}
}
}
async function createNoteFromAnnotation(
annotationItem: Zotero.Item,
openMode: "standalone" | "auto" = "auto"
) {
const annotationTags = annotationItem.getTags().map((_) => _.tag);
const linkRegex = new RegExp("^zotero://note/(.*)$");
for (const tag of annotationTags) {
if (linkRegex.test(tag)) {
const linkParams = getNoteLinkParams(tag);
if (linkParams.noteItem) {
addon.hooks.onOpenNote(linkParams.noteItem.id, openMode, {
lineIndex: linkParams.lineIndex || undefined,
});
return;
} else {
annotationItem.removeTag(tag);
await annotationItem.saveTx();
}
}
}
const note: Zotero.Item = new Zotero.Item("note");
note.libraryID = annotationItem.libraryID;
note.parentID = annotationItem.parentItem!.parentID;
await note.saveTx();
// await waitUtilAsync(() => Boolean(getEditorInstance(note.id)));
const renderredTemplate = await addon.api.template.runTemplate(
"[QuickNoteV5]",
"annotationItem, topItem, noteItem",
[annotationItem, annotationItem.parentItem!.parentItem, note]
);
await addLineToNote(note, renderredTemplate);
const tags = annotationItem.getTags();
for (const tag of tags) {
note.addTag(tag.tag, tag.type);
}
await note.saveTx();
ZoteroPane.openNoteWindow(note.id);
annotationItem.addTag(getNoteLink(note)!);
await annotationItem.saveTx();
}
// async function OCRImageAnnotation(src: string, annotationItem: Zotero.Item) {
// /*
// message.content = {
// params: { src: string, annotationItem: Zotero.Item }
// }
// */
// let result: string;
// let success: boolean;
// const engine = Zotero.Prefs.get("Knowledge4Zotero.OCREngine");
// if (engine === "mathpix") {
// const xhr = await Zotero.HTTP.request(
// "POST",
// "https://api.mathpix.com/v3/text",
// {
// headers: {
// "Content-Type": "application/json; charset=utf-8",
// app_id: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appid"),
// app_key: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appkey"),
// },
// body: JSON.stringify({
// src: src,
// math_inline_delimiters: ["$", "$"],
// math_display_delimiters: ["$$", "$$"],
// rm_spaces: true,
// }),
// responseType: "json",
// }
// );
// this._Addon.toolkit.Tool.log(xhr);
// if (xhr && xhr.status && xhr.status === 200 && xhr.response.text) {
// result = xhr.response.text;
// success = true;
// } else {
// result = xhr.status === 200 ? xhr.response.error : `${xhr.status} Error`;
// success = false;
// }
// } else if (engine === "xunfei") {
// /**
// * 1.Dochttps://www.xfyun.cn/doc/words/formula-discern/API.html
// * 2.Error codehttps://www.xfyun.cn/document/error-code
// * @author iflytek
// */
// const config = {
// hostUrl: "https://rest-api.xfyun.cn/v2/itr",
// host: "rest-api.xfyun.cn",
// appid: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APPID"),
// apiSecret: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APISecret"),
// apiKey: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APIKey"),
// uri: "/v2/itr",
// };
// let date = new Date().toUTCString();
// let postBody = getPostBody();
// let digest = getDigest(postBody);
// const xhr = await Zotero.HTTP.request("POST", config.hostUrl, {
// headers: {
// "Content-Type": "application/json",
// Accept: "application/json,version=1.0",
// Host: config.host,
// Date: date,
// Digest: digest,
// Authorization: getAuthStr(date, digest),
// },
// body: JSON.stringify(postBody),
// responseType: "json",
// });
// if (xhr?.response?.code === 0) {
// result = xhr.response.data.region
// .filter((r) => r.type === "text")
// .map((r) => r.recog.content)
// .join(" ")
// .replace(/ifly-latex-(begin)?(end)?/g, "$");
// this._Addon.toolkit.Tool.log(xhr);
// success = true;
// } else {
// result =
// xhr.status === 200
// ? `${xhr.response.code} ${xhr.response.message}`
// : `${xhr.status} Error`;
// success = false;
// }
// function getPostBody() {
// let digestObj = {
// common: {
// app_id: config.appid,
// },
// business: {
// ent: "teach-photo-print",
// aue: "raw",
// },
// data: {
// image: src.split(",").pop(),
// },
// };
// return digestObj;
// }
// function getDigest(body) {
// return (
// "SHA-256=" +
// CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(JSON.stringify(body)))
// );
// }
// function getAuthStr(date, digest) {
// let signatureOrigin = `host: ${config.host}\ndate: ${date}\nPOST ${config.uri} HTTP/1.1\ndigest: ${digest}`;
// let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
// let signature = CryptoJS.enc.Base64.stringify(signatureSha);
// let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"`;
// return authorizationOrigin;
// }
// } else if (engine === "bing") {
// const xhr = await Zotero.HTTP.request(
// "POST",
// "https://www.bing.com/cameraexp/api/v1/getlatex",
// {
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// data: src.split(",").pop(),
// inputForm: "Image",
// clientInfo: { platform: "edge" },
// }),
// responseType: "json",
// }
// );
// if (xhr && xhr.status && xhr.status === 200 && !xhr.response.isError) {
// result = xhr.response.latex
// ? `$${xhr.response.latex}$`
// : xhr.response.ocrText;
// success = true;
// } else {
// result =
// xhr.status === 200 ? xhr.response.errorMessage : `${xhr.status} Error`;
// success = false;
// }
// } else {
// result = "OCR Engine Not Found";
// success = false;
// }
// if (success) {
// annotationItem.annotationComment = `${
// annotationItem.annotationComment
// ? `${annotationItem.annotationComment}\n`
// : ""
// }${result}`;
// await annotationItem.saveTx();
// this._Addon.ZoteroViews.showProgressWindow(
// "Better Notes OCR",
// `OCR Result: ${result}`
// );
// } else {
// this._Addon.ZoteroViews.showProgressWindow(
// "Better Notes OCR",
// result,
// "fail"
// );
// }
// }

434
src/modules/sync/api.ts Normal file
View File

@ -0,0 +1,434 @@
import YAML = require("yamljs");
import { showHint } from "../../utils/hint";
import { getNoteLinkParams } from "../../utils/link";
import { clearPref, getPref, setPref } from "../../utils/prefs";
import { getString } from "../../utils/locale";
export {
getRelatedNoteIds,
removeSyncNote,
isSyncNote,
getSyncNoteIds,
addSyncNote,
updateSyncStatus,
doSync,
setSync,
getSyncStatus,
getNoteStatus,
getMDStatus,
getMDStatusFromContent,
getMDFileName,
};
function getSyncNoteIds(): number[] {
const ids = getPref("syncNoteIds") as string;
return Zotero.Items.get(ids.split(",").map((id: string) => Number(id)))
.filter((item) => item.isNote())
.map((item) => item.id);
}
function isSyncNote(noteId: number): boolean {
const syncNoteIds = getSyncNoteIds();
return syncNoteIds.includes(noteId);
}
async function getRelatedNoteIds(noteId: number): Promise<number[]> {
let allNoteIds: number[] = [noteId];
const note = Zotero.Items.get(noteId);
const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g);
if (!linkMatches) {
return allNoteIds;
}
const subNoteIds = (
await Promise.all(
linkMatches.map(async (link) => getNoteLinkParams(link).noteItem)
)
)
.filter((item) => item && item.isNote())
.map((item) => (item as Zotero.Item).id);
allNoteIds = allNoteIds.concat(subNoteIds);
allNoteIds = new Array(...new Set(allNoteIds));
return allNoteIds;
}
async function getRelatedNoteIdsFromNotes(
noteIds: number[]
): Promise<number[]> {
let allNoteIds: number[] = [];
for (const noteId of noteIds) {
allNoteIds = allNoteIds.concat(await getRelatedNoteIds(noteId));
}
return allNoteIds;
}
function addSyncNote(noteId: number) {
const ids = getSyncNoteIds();
if (ids.includes(noteId)) {
return;
}
ids.push(noteId);
setPref("syncNoteIds", ids.join(","));
}
function removeSyncNote(noteId: number) {
const ids = getSyncNoteIds();
setPref("syncNoteIds", ids.filter((id) => id !== noteId).join(","));
clearPref(`syncDetail-${noteId}`);
}
function updateSyncStatus(noteId: number, status: SyncStatus) {
addSyncNote(noteId);
setPref(`syncDetail-${noteId}`, JSON.stringify(status));
}
function getNoteStatus(noteId: number) {
const noteItem = Zotero.Items.get(noteId);
if (!noteItem?.isNote()) {
return;
}
const fullContent = noteItem.getNote();
const ret = {
meta: "",
content: "",
tail: "</div>",
lastmodify: Zotero.Date.sqlToDate(noteItem.dateModified, true),
};
const metaRegex = /"?data-schema-version"?="[0-9]*">/;
const match = fullContent?.match(metaRegex);
if (!match || match.length == 0) {
ret.meta = '<div "data-schema-version"="9">';
ret.content = fullContent || "";
return ret;
}
const idx = fullContent.search(metaRegex);
if (idx != -1) {
ret.content = fullContent.substring(
idx + match[0].length,
fullContent.length - ret.tail.length
);
}
return ret;
}
function getSyncStatus(noteId?: number): SyncStatus {
const defaultStatus = JSON.stringify({
path: "",
filename: "",
md5: "",
noteMd5: "",
lastsync: new Date().getTime(),
itemID: -1,
});
return JSON.parse(
(getPref(`syncDetail-${noteId}`) as string) || defaultStatus
);
}
function getMDStatusFromContent(contentRaw: string): MDStatus {
const result = contentRaw.match(/^---([\s\S]*)---\n/);
const ret: MDStatus = {
meta: { version: -1 },
content: contentRaw,
filedir: "",
filename: "",
lastmodify: new Date(0),
};
if (result) {
const yaml = result[0].replace(/---/g, "");
ret.content = contentRaw.slice(result[0].length);
try {
ret.meta = YAML.parse(yaml);
} catch (e) {
ztoolkit.log(e);
}
}
return ret;
}
async function getMDStatus(
source: Zotero.Item | number | string
): Promise<MDStatus> {
let ret: MDStatus = {
meta: null,
content: "",
filedir: "",
filename: "",
lastmodify: new Date(0),
};
try {
let filepath = "";
if (typeof source === "string") {
filepath = source;
} else if (typeof source === "number") {
const syncStatus = getSyncStatus(source);
filepath = `${syncStatus.path}/${syncStatus.filename}`;
} else if (source.isNote && source.isNote()) {
const syncStatus = getSyncStatus(source.id);
filepath = `${syncStatus.path}/${syncStatus.filename}`;
}
filepath = Zotero.File.normalizeToUnix(filepath);
if (await OS.File.exists(filepath)) {
let contentRaw = (await OS.File.read(filepath, {
encoding: "utf-8",
})) as string;
ret = getMDStatusFromContent(contentRaw);
const pathSplit = filepath.split("/");
ret.filedir = Zotero.File.normalizeToUnix(
pathSplit.slice(0, -1).join("/")
);
ret.filename = filepath.split("/").pop() || "";
const stat = await OS.File.stat(filepath);
ret.lastmodify = stat.lastModificationDate;
}
} catch (e) {
ztoolkit.log(e);
}
return ret;
}
async function getMDFileName(noteId: number, searchDir?: string) {
const noteItem = Zotero.Items.get(noteId);
if (searchDir !== undefined && (await OS.File.exists(searchDir))) {
const mdRegex = /\.(md|MD|Md|mD)$/;
let matchedFileName = null;
let matchedDate = new Date(0);
await Zotero.File.iterateDirectory(
searchDir,
async (entry: OS.File.Entry) => {
if (entry.isDir) return;
if (mdRegex.test(entry.name)) {
if (
entry.name.split(".").shift()?.split("-").pop() === noteItem.key
) {
const stat = await OS.File.stat(entry.path);
if (stat.lastModificationDate > matchedDate) {
matchedFileName = entry.name;
matchedDate = stat.lastModificationDate;
}
}
}
}
);
if (matchedFileName) {
return matchedFileName;
}
}
return await addon.api.template.runTemplate(
"[ExportMDFileNameV2]",
"noteItem",
[noteItem]
);
}
function setSync() {
const syncPeriod = getPref("syncPeriodSeconds") as number;
if (syncPeriod > 0) {
showHint(`${getString("sync.start.hint")} ${syncPeriod} s`);
const timer = ztoolkit.getGlobal("setInterval")(() => {
if (!addon.data.alive) {
showHint(getString("sync.stop.hint"));
ztoolkit.getGlobal("clearInterval")(timer);
}
// Only when Zotero is active and focused
if (document.hasFocus() && (getPref("syncPeriodSeconds") as number) > 0) {
doSync(undefined, { quiet: true, skipActive: true, reason: "auto" });
}
}, Number(syncPeriod) * 1000);
}
}
async function doCompare(noteItem: Zotero.Item): Promise<SyncCode> {
const syncStatus = getSyncStatus(noteItem.id);
const MDStatus = await getMDStatus(noteItem.id);
// No file found
if (!MDStatus.meta) {
return SyncCode.NoteAhead;
}
// File meta is unavailable
if (MDStatus.meta.version < 0) {
return SyncCode.NeedDiff;
}
let MDAhead = false;
let noteAhead = false;
const md5 = Zotero.Utilities.Internal.md5(MDStatus.content, false);
const noteMd5 = Zotero.Utilities.Internal.md5(noteItem.getNote(), false);
// MD5 doesn't match (md side change)
if (md5 !== syncStatus.md5) {
MDAhead = true;
}
// MD5 doesn't match (note side change)
if (noteMd5 !== syncStatus.noteMd5) {
noteAhead = true;
}
// Note version doesn't match (note side change)
// This might be unreliable when Zotero account is not login
if (Number(MDStatus.meta.version) !== noteItem.version) {
noteAhead = true;
}
if (noteAhead && MDAhead) {
return SyncCode.NeedDiff;
} else if (noteAhead) {
return SyncCode.NoteAhead;
} else if (MDAhead) {
return SyncCode.MDAhead;
} else {
return SyncCode.UpToDate;
}
}
async function doSync(
items: Zotero.Item[] = [],
{ quiet, skipActive, reason } = {
quiet: true,
skipActive: true,
reason: "unknown",
}
) {
// Always log in development mode
if (addon.data.env === "development") {
quiet = false;
}
if (addon.data.sync.lock) {
// Only allow one task
return;
}
let progress;
// Wrap the code in try...catch so that the lock can be released anyway
try {
addon.data.sync.lock = true;
let skippedCount = 0;
if (!items || !items.length) {
items = Zotero.Items.get(getSyncNoteIds());
}
if (skipActive) {
// Skip active note editors' targets
const activeNoteIds = Zotero.Notes._editorInstances
.filter((editor) => editor._iframeWindow.document.hasFocus())
.map((editor) => editor._item.id);
const filteredItems = items.filter(
(item) => !activeNoteIds.includes(item.id)
);
skippedCount = items.length - filteredItems.length;
items = filteredItems;
}
ztoolkit.log("sync start", reason, items, skippedCount);
if (!quiet) {
progress = new ztoolkit.ProgressWindow(
`[${getString("sync.running.hint.title")}] ${
addon.data.env === "development" ? reason : "Better Notes"
}`
)
.createLine({
text: `[${getString("sync.running.hint.check")}] 0/${
items.length
} ...`,
type: "default",
progress: 1,
})
.show(-1);
}
// Export items of same dir in batch
const toExport = {} as Record<string, number[]>;
const toImport: SyncStatus[] = [];
const toDiff: SyncStatus[] = [];
let i = 1;
for (const item of items) {
const syncStatus = getSyncStatus(item.id);
const filepath = syncStatus.path;
let compareResult = await doCompare(item);
switch (compareResult) {
case SyncCode.NoteAhead:
if (Object.keys(toExport).includes(filepath)) {
toExport[filepath].push(item.id);
} else {
toExport[filepath] = [item.id];
}
break;
case SyncCode.MDAhead:
toImport.push(syncStatus);
break;
case SyncCode.NeedDiff:
toDiff.push(syncStatus);
break;
default:
break;
}
progress?.changeLine({
text: `[${getString("sync.running.hint.check")}] ${i}/${
items.length
} ...`,
progress: ((i - 1) / items.length) * 100,
});
i += 1;
}
ztoolkit.log("will be synced:", toExport, toImport, toDiff);
i = 1;
let totalCount = Object.keys(toExport).length;
for (const filepath of Object.keys(toExport)) {
progress?.changeLine({
text: `[${getString("sync.running.hint.updateMD")}] ${i}/${
items.length
} ...`,
progress: ((i - 1) / items.length) * 100,
});
await addon.api._export.syncMDBatch(filepath, toExport[filepath]);
i += 1;
}
i = 1;
totalCount = toImport.length;
for (const syncStatus of toImport) {
progress?.changeLine({
text: `[${getString(
"sync.running.hint.updateNote"
)}] ${i}/${totalCount}, ${toDiff.length} queuing...`,
progress: ((i - 1) / totalCount) * 100,
});
const item = Zotero.Items.get(syncStatus.itemID);
const filepath = OS.Path.join(syncStatus.path, syncStatus.filename);
await addon.api._import.fromMD(filepath, { noteId: item.id });
// Update md file to keep the metadata synced
await addon.api._export.syncMDBatch(syncStatus.path, [item.id]);
i += 1;
}
i = 1;
totalCount = toDiff.length;
for (const syncStatus of toDiff) {
progress?.changeLine({
text: `[${getString("sync.running.hint.diff")}] ${i}/${totalCount}...`,
progress: ((i - 1) / totalCount) * 100,
});
await addon.api.window.showSyncDiff(
syncStatus.itemID,
OS.Path.join(syncStatus.path, syncStatus.filename)
);
i += 1;
}
const syncCount =
Object.keys(toExport).length + toImport.length + toDiff.length;
progress?.changeLine({
text:
(syncCount
? `[${getString(
"sync.running.hint.finish"
)}] ${syncCount} ${getString("sync.running.hint.synced")}`
: `[${getString("sync.running.hint.finish")}] ${getString(
"sync.running.hint.upToDate"
)}`) + (skippedCount ? `, ${skippedCount} skipped.` : ""),
progress: 100,
});
} catch (e) {
ztoolkit.log(e);
showHint(`Sync Error: ${String(e)}`);
} finally {
progress?.startCloseTimer(5000);
}
addon.data.sync.lock = false;
}
enum SyncCode {
UpToDate = 0,
NoteAhead,
MDAhead,
NeedDiff,
}

Some files were not shown because too many files have changed in this diff Show More