1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

148 Commits

Author SHA1 Message Date
Laurent Cozic
715253da2f Android release v1.0.316 2020-01-24 22:34:02 +00:00
Laurent Cozic
66356d83ab Electron release v1.0.179 2020-01-24 22:29:17 +00:00
Laurent Cozic
8e531ca87a Update translations 2020-01-24 22:28:54 +00:00
Laurent Cozic
18c46851fd Desktop: Fixes #2324: Apply userstyle again when exporting to PDF or printing 2020-01-24 21:46:48 +00:00
Laurent Cozic
5456dbbf16 Merge branch 'master' of github.com:laurent22/joplin 2020-01-24 21:16:33 +00:00
Laurent Cozic
5c54b83108 Desktop: Fixed update message box 2020-01-24 21:16:25 +00:00
Laurent Cozic
cbf7f03bff Desktop: Improve appearance of note Info dialog 2020-01-24 21:16:18 +00:00
mic704b
ea05fea234 Desktop: Fix identification of note in pdf export from main menu. (#2323) 2020-01-24 20:57:11 +00:00
mic704b
f78729ad1f Cross-platform version number retrieval (#2355)
* Add shim to retrieve application version.

* Create shim to retrieve app version number.

* Add a fall through handler to throw an error.
2020-01-24 20:56:44 +00:00
genneko
4ec9492f7c Update ja_JP.po (#2358) 2020-01-24 15:19:57 +00:00
bedwardly-down
f86b953420 Tools: Updated Request to v2.88.0 to remove dependency on Outdated and Insecure Hawk and Cryptiles Dependencies (#2353)
* Updated Cryptiles dependency to 3.1.4

The maintainer is no longer supporting any version of cryptiles before
v4.2.0 on npm; npm's older versions stop at 3.1.2 and a high security
vulnerability was thrown during a build on linux: https://npmjs.com/advisories/720

The maintainer still has 3.1.4 tagged in his repo and this commit
updates cryptiles to solve that issue without breaking compatibility
since it was primarily bug fixes for a code freeze.

* Updated Request Dependency to 2.86.0

* Updated Reqiest to 2.88.0
2020-01-24 15:19:22 +00:00
Laurent Cozic
6563606799 Merge branch 'master' of github.com:laurent22/joplin 2020-01-23 17:43:16 +00:00
Laurent Cozic
c01bc1c363 All: Added new, more secure encryption methods, so that they can be switched to at a later time 2020-01-22 22:01:58 +00:00
Laurent Cozic
6f8c634756 Tools: Add developer names to changelog 2020-01-22 20:33:43 +00:00
Alexander Pankratov
22a93994aa Translation: Update ru_RU.po (#2347) 2020-01-23 01:07:12 -05:00
Xaris Ar
e0013858c4 Translation: Update el_GR.po (#2340)
* Create el_GR.po (part1)

* Update el_GR.po

* Update el_GR.po (part 2)

* Update el_GR.po

* Finished Greek(el_GR) translation

Finished translating all texts.
Update el_GR.po (beta)

* Update el_GR.po

to meet requirements for Joplin 1.0.173

* Update locale.js

* Delete el_GR.po

* Create el_GR.po

* Update Greek Translation

* Update Greek Translation
2020-01-22 22:31:19 -05:00
Laurent Cozic
b6e0df57eb Desktop: Fixes #2352: undefined text was being displayed on top of notes in revision viewer 2020-01-22 17:32:21 +00:00
Laurent Cozic
be210233be Desktop, Mobile: Fixes #2339, Fixes #2343, Fixes #2345: Fixed issues with Katex and MultiMd table plugin 2020-01-22 17:16:37 +00:00
Carl Bordum Hansen
1a1a1d3841 Desktop: Fix Linux installation script (#2333) 2020-01-22 13:47:20 +00:00
Laurent Cozic
4283bbde7f Doc: Fixed changelog generation for API 2020-01-21 10:40:29 +00:00
Laurent Cozic
fba325f60e Doc: Fixed APIdoc 2020-01-21 09:44:46 +00:00
Laurent Cozic
fcd76dabac Update website 2020-01-21 09:42:27 +00:00
Helmut K. C. Tessarek
e9366a0d41 Update translations 2020-01-20 19:11:57 -05:00
Laurent Cozic
953aa5d0b5 Electron release v1.0.178 2020-01-20 18:44:23 +00:00
Laurent Cozic
3fed1abc36 API: Add ability to search by folder or tag title 2020-01-20 02:19:57 +00:00
Alexander Teterkin
6973bf9331 Documentation: Added 'Yandex Disk' to the list of WebDAV-compatible services known to work with Joplin. (#2285)
* Add Yandex Disk to the list of WebDAV-compatible services

Yandex Disk (cloud storage by Yandex) supports WebDAV access and is known to work with Joplin.

* Add Yandex Disk to the list of WebDAV-compatible services

Added 'Yandex Disk' to the list of WebDAV-compatible services known to work with Joplin (now in correct alphabetical order).
2020-01-20 01:02:54 -05:00
Laurent Cozic
e8867fa0f1 Doc: Added CalebJohn as mentor on three ideas 2020-01-19 16:24:13 +00:00
Bart
d9c15b84d0 Desktop: when importing MD files create resources for local linked files (#2262)
* md importer: first pass import attachment resources with markdown files

* md importer: import resources from md - no unneeded saves, check if files exist, regex name

* md importer: test import of local files as resources, separate method for importing linked files, comment regex matching md tags

* md importer: move stateful regex to method scope, remove spurius await

* md importer: lint

* md importer: respond to PR comments: remove test nesting, test sample, check if path is dir, use shim.fsDriver

* md importer: use file-path methods for getting attachment path

* md importer: use extractImageUrls helper, test for file with zero links

* md importer: try catch around importLocalImages, improve test

* md importer: importing attached images cover case where link also appears elsewhere in doc

* md importer: only create 1 resource if note contains duplicate links, test

* md importer: remove log

* md importer: remove use of lodash
2020-01-19 15:39:38 +00:00
Helmut K. C. Tessarek
81876c7bf3 Desktop: Update Electron to 7.1.9 (#2314)
This is only a minor bump but several issues have been fixed since 7.1.5.

The most pressing one that we've experienced for a long time:

default button in dialog not working on macOS
https://github.com/electron/electron/issues/21633
2020-01-19 15:34:45 +00:00
Laurent Cozic
1dd7727e97 Doc: Fixed GitHub icon 2020-01-18 15:06:35 +00:00
Laurent Cozic
fe0318584e Update website 2020-01-18 14:54:10 +00:00
Laurent Cozic
8508fe737b Doc: Added GitHub Sponsor icon 2020-01-18 14:53:49 +00:00
Laurent Cozic
c7a9e5f656 Merge branch 'master' of github.com:laurent22/joplin 2020-01-18 14:27:26 +00:00
Laurent Cozic
3e43fbce13 Doc: Fix GSoC doc 2020-01-18 14:27:21 +00:00
mic704b
b304e2ae1f Desktop: Fix bug in note tags display due to error in comparison of tag lists. (#2302) 2020-01-18 13:55:35 +00:00
lightray22
35f4ede11a Desktop: show completed date in note properties (#2292) 2020-01-18 13:53:59 +00:00
mic704b
65cbb6e388 Desktop: Maintain selection when non-selected note is deleted (#2290)
* Fix jump of focus following deletion action.

Applies to notes, folders and tags.

* Add tests for reducer item delete handling.

* Add comments.

* Clean up.
2020-01-18 13:53:00 +00:00
lightray22
960d7f84eb Desktop: Don't count completed to-dos in note counts when they are not shown (#2288)
* Desktop: don't count completed to-dos in note counts when they are not shown

* Desktop: review comments for commit 0383dcc

* Desktop: fix remaining lint issues with commit 1fe4685
2020-01-18 13:46:04 +00:00
mic704b
8a392e1c06 Desktop: Fixes #2254: Fix pdf export when mouse over non-selected note in notelist. (#2255) 2020-01-18 13:30:15 +00:00
Vaidotas Simkus
d9d75d6c71 Desktop, Cli: Replace note links with relative paths in MD Exporter (#2161)
* Replace linked Note ids by relative paths in MD Exporter.

* Added tests for the MD Exporter.

* Changed fs.readdirSync use for earlier Node version (v8)

In the previous commit the code used fs.readdirSync from Node v10 or
later. But since Joplin still uses v8, I changed the use of
fs.readdirSync to be in line with the earlier api.

* Updated readDirSync use for Node v10, which allows gets folder names too.

* Revert "Updated readDirSync use for Node v10, which allows gets folder names too."

This reverts commit 8f255db120861dd7773d99e1b63f4864d39594cf.
Because the Travis builds still use Node v8. This is fine as well, the
readdirSync returns the filenames in the directory.

* Added reservedNames param to findUniqueFilename
2020-01-18 13:16:14 +00:00
Laurent Cozic
69f9e38730 Doc: Improved test unit doc 2020-01-18 13:11:42 +00:00
Laurent Cozic
7f95186a97 Doc: Updated GSoC ideas and added section about abandoned pull requests 2020-01-18 13:08:23 +00:00
Laurent Cozic
6f976abf42 Doc: Added "Custom keyboard shortcuts" idea for GSoC 2020-01-17 10:53:53 +00:00
Laurent Cozic
d80ffeeba1 Doc: Update mentors for GSoC 2020-01-17 10:48:35 +00:00
zen-quo
e078de25f0 Translation: Update tr_TR.po (#2295) 2020-01-12 19:01:00 -05:00
Laurent Cozic
cd284f78ad Update website 2020-01-11 16:58:32 +00:00
Laurent Cozic
0a13c988fa Mobile: Fixes #1816: When creating a new note, it was not possible to focus the body text field 2020-01-08 17:42:28 +00:00
Laurent Cozic
b61bfd6ffe Android: Fixes #2270: Note files could become corrupted when using file system sync on certain Android versions 2020-01-08 18:57:40 +00:00
Laurent Cozic
fc61b474cd Clipper: Fixes #2252: Some pages that contain tables with only one cell would trigger an error 2020-01-08 18:35:41 +00:00
Laurent Cozic
bf25364333 Clipper: Fixes #2267: Fixed race condition when importing page that have multiple images with similar names 2020-01-08 18:21:13 +00:00
Laurent Cozic
bc7099d29b Desktop, Mobile: Fixed regression in HTML note rendering 2020-01-08 18:05:13 +00:00
Laurent Cozic
00c3ed715c Update PT translation 2020-01-07 15:44:54 +00:00
Caleb John
701b57de89 Fix casing typo for PluginAssetsLoader.js in gitignore (#2250)
* Fix casing typo for PluginAssetsLoader.js in gitignore

* Add change to eslintignore
2020-01-07 00:47:51 +00:00
Caleb John
e674d7d23b Desktop: Add option to disable auto-matching braces (#2251)
* Add option to disable auto-matching braces

* Only Make option desktop only
2020-01-06 22:27:37 +00:00
Devon Zuegel
4a2d9bb028 CLI: Upgrade sqlite (#2248)
* Upgrade sqlite

* Add info about sqlite installation to docs
2020-01-06 22:24:38 +00:00
Abijeet Patro
ae3a278ac4 Desktop: New: Display selected tags under a note title (#2217)
Follow up to #893

Now using middleware to set the tags when a note is selected

This avoids the ugly code in the NoteTextComponent where we determine
if tags are to be fetched, identify if they have been modified, fetch
them  and then dispatch an action to update the store which might
again re-render the component.

Also implements style related fixes from #1000

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
Fixes: #469
2020-01-06 21:23:22 +00:00
mic704b
42ada7123c Desktop: Add external editor actions to the note context menu. (#2214)
* Add external editor actions to the note context menu.

Also start up external editor on note double click.

These changes enhance user experience by placing the actions where
they feel natural.

* Remove double-click behaviour and change menu text.

Changes in response to review comments.

* Move handling of external editor actions to main screen from note text

This is to ensure correct behaviour even when the user launches the
action on a note in the list that is under the pointer, but not selected.

* Move external edit actions to NoteListUtils from MainScreen.

* Reconnect external edit action in main edit menu.
2020-01-06 21:16:39 +00:00
Laurent Cozic
6d9f73eef7 Improved integration of external renderer 2019-12-30 21:54:13 +01:00
Laurent Cozic
541372eb91 Merge branch 'master' of github.com:laurent22/joplin 2019-12-30 20:54:53 +01:00
Laurent Cozic
8d7d70bc13 Desktop: Fixed export to HTML, PDF and printing 2019-12-30 20:44:15 +01:00
Scott Bronson
e77cc18468 Documentation: Fix broken link (#2245)
The folder is `gsoc2020`, not just `gsoc`
2019-12-30 12:42:44 -05:00
Laurent Cozic
193978a8be Android release v1.0.315 2019-12-30 15:16:55 +01:00
Laurent Cozic
853ac0cca8 Electron release v1.0.177 2019-12-30 15:11:34 +01:00
Laurent Cozic
589f0803e6 Fixed Electron upgrade regressions 2019-12-30 15:10:43 +01:00
Laurent Cozic
fc67a44f95 Desktop: Fixed scrolling issue when clicking on anchor 2019-12-30 13:00:53 +01:00
Laurent Cozic
204365b2ae Tools: Fixed desktop build 2019-12-30 10:54:31 +01:00
Laurent Cozic
2a63ecef2a All: Extract note renderer to separate package (WIP) (#2206)
* Started updating to use external renderer package

* Added way to build renderer assets

* Done mobile compatilibty

* Upgrade joplin-renderer

* Added joplin-renderer package
2019-12-29 18:58:40 +01:00
2jaeyeol
1d660d7141 Translation: update ko.po (#2227)
* translate_korean

* translate_korean2
2019-12-29 11:20:54 +01:00
Ibrahim AHMED BACHA
69000c0fc5 Translation: update ar.po (#2231) 2019-12-29 08:52:41 +01:00
Rafael Cavalcanti
c8a0138b3b Desktop: Fixes #2122: Fix Goto Anything scrolling issue (#2199)
* Fix Goto scrolling (#2122)

* Better fix to Goto scrolling (#2122)

* Fix #2122: fix bottomItemIndex and top values
2019-12-28 22:53:21 +01:00
Marcus Hill
90de63e650 CLI: Add --export, --import, and --import-file flags to joplin config (#2179)
* Add --export, --import, and --import-file flags to joplin config

* Convert config --export/--import to work with JSON

* Remove unnecessary check in renderKeyValue
2019-12-28 22:48:34 +01:00
Laurent Cozic
6b6e17cbad Mobile: Display warning box when a resource cannot be downloaded 2019-12-28 20:50:06 +01:00
Laurent Cozic
071bd2b0ca Android release v1.0.314 2019-12-28 20:28:11 +01:00
Laurent Cozic
f74db06176 All: Better handling of resource download errors, and added resource info to sync status screen 2019-12-28 20:23:38 +01:00
Laurent Cozic
a6b3ddc7ed Android release v1.0.313 2019-12-28 18:58:04 +01:00
Laurent Cozic
4ff889d4ec Android: Added button to export profile to external SD card for debugging 2019-12-28 18:47:37 +01:00
Laurent Cozic
12b9f1b969 Merge branch 'master' of github.com:laurent22/joplin 2019-12-28 17:57:21 +01:00
Helmut K. C. Tessarek
59bb1015ab fix Nextcloud name (NextCloud -> Nextcloud) 2019-12-25 11:37:50 +01:00
oscaretu
f9c77171cf Translation: Update es_ES.po (#2211) 2019-12-23 12:32:23 +01:00
Ethan Chen
9628b64d3e Translation: Update zh_TW.po (#2215) 2019-12-23 12:30:41 +01:00
Laurent Cozic
d3f47a38b8 Update README.md 2019-12-20 21:47:06 +00:00
genneko
8111213691 Translation: Update ja_JP.po (#2210) 2019-12-20 11:46:32 +01:00
Laurent Cozic
a88ff902b4 Merge branch 'master' of github.com:laurent22/joplin 2019-12-19 15:19:29 +00:00
Helmut K. C. Tessarek
1e57e1e486 Translation: update es_ES.po
closes #2205
2019-12-19 12:59:17 +01:00
Joel Taylor
172afb0789 Cli: Update CliClient node dependency to 10+ (#2177) 2019-12-18 22:25:14 +00:00
Helmut K. C. Tessarek
5bfd1849c1 Desktop, Mobile: Update Katex to 0.11.1 (#2201)
* update Katex to 0.11.1

* add package-lock.json files
2019-12-18 22:23:32 +00:00
Helmut K. C. Tessarek
f61c9c93bb update firefox addon link
closes #2203
2019-12-18 18:02:58 +01:00
Laurent Cozic
b0efdb6ee8 Merge branch 'master' of github.com:laurent22/joplin 2019-12-18 16:05:02 +00:00
Laurent Cozic
888a9ddaf4 Desktop: Improved Nextcloud API error handling 2019-12-18 15:32:19 +00:00
Helmut K. C. Tessarek
d2482d6554 Translation: update de_DE.po 2019-12-18 14:55:43 +01:00
Helmut K. C. Tessarek
21cac248b3 Translation: update en_US.po 2019-12-18 14:09:08 +01:00
Laurent Cozic
ce7671151c Desktop: Remove useless React warnings from console 2019-12-18 11:49:44 +00:00
Helmut K. C. Tessarek
b77525e570 Update translations 2019-12-18 12:45:10 +01:00
Laurent Cozic
e93cc50d1c Translation: add Portuguese (pt_PT.po) (Thanks Diogo Caveiro) (#2194)
Author name: Diogo Caveiro
2019-12-18 12:42:49 +01:00
Laurent Cozic
c534305c7b Desktop: Fixes #2157: Prevent app from crashing when pressing focus shortcut from search bar 2019-12-18 11:16:13 +00:00
Laurent Cozic
797b71d903 Doc: Fixes #2187: Fixed API doc 2019-12-18 11:00:52 +00:00
Laurent Cozic
74fd9e1e9e All: Fixes #2091: Handle WebDAV servers that do not return a last modified date (fixes mail.ru) 2019-12-18 10:46:12 +00:00
Laurent Cozic
ff94a95589 Desktop: Fixes #2144: Fix notifications on Windows 7 2019-12-18 10:22:01 +00:00
Laurent Cozic
0f5192bf19 Tools: Display script line in translation files 2019-12-18 10:21:36 +00:00
Laurent Cozic
eabbbba0c7 Desktop: Fixed HTML export 2019-12-18 10:00:59 +00:00
Laurent Cozic
840cdf5512 Doc: Added troubleshooting section to BUILD.md for Windows development 2019-12-18 09:34:03 +00:00
Laurent Cozic
757a6854ab Desktop: Updated OneDrive login to remove webview dependency 2019-12-18 09:21:12 +00:00
Laurent Cozic
b16dd051f1 Merge branch 'master' of github.com:laurent22/joplin 2019-12-17 17:07:15 +00:00
Laurent Cozic
68e73b658a Desktop: Fixed dev tool support 2019-12-17 17:06:55 +00:00
Laurent Cozic
af5f301276 Update ideas.md 2019-12-17 14:51:25 +00:00
Laurent Cozic
2b9818a94d Merge branch 'master' of github.com:laurent22/joplin 2019-12-17 12:49:16 +00:00
Laurent Cozic
58200ecdb1 Desktop: Decrypt notes that are meant to be shared 2019-12-17 12:45:57 +00:00
Ladislav Benc
acaf22fa11 Doc: Fixing a couple typos (#2198) 2019-12-17 12:22:28 +00:00
Laurent Cozic
f60d6e0748 Desktop: Make it easier to view early errors when the app starts 2019-12-17 12:09:57 +00:00
Helmut K. C. Tessarek
ae9163e9bb add description for soft breaks to plugin section of markdown page
closes #2192
2019-12-17 12:55:46 +01:00
Laurent Cozic
cad6b7971f Desktop: Upgrade to Electron 7 2019-12-17 11:08:55 +00:00
Laurent Cozic
ee38590c35 Allow printing and creating PDF from iframe 2019-12-17 09:44:48 +00:00
Laurent Cozic
f10695fb8f Desktop: Render note using iframe instead of deprecated webview 2019-12-17 00:45:27 +00:00
Laurent Cozic
b44ecc1958 Tools: Added build scripts for Windows 2019-12-17 00:40:49 +00:00
Laurent Cozic
931e7a7795 Improved export to HTML when note is already HTML 2019-12-17 00:40:25 +00:00
Laurent Cozic
df85bb189d Update website 2019-12-16 23:22:46 +00:00
Laurent Cozic
27e1f53b5f Doc: More clean up for GSoC 2019-12-16 23:22:28 +00:00
Laurent Cozic
266ddedaef Update website 2019-12-16 23:19:45 +00:00
Laurent Cozic
44dd327d22 Doc: More clean up for GSoC 2019-12-16 23:18:23 +00:00
Laurent Cozic
13be56a2e3 Doc: Moved all GSoC ideas to single document 2019-12-16 22:53:45 +00:00
Laurent Cozic
5d0ba460ae Update website 2019-12-16 17:22:44 +00:00
Laurent Cozic
6988b3accb Doc: Added idea: Multi-profile support, for GSoC 2019-12-16 17:21:35 +00:00
Laurent Cozic
3e2676a8c6 Doc: Removed number from GSoC ideas 2019-12-16 16:51:36 +00:00
Laurent Cozic
0f88c947f1 Doc: Cleaned up GSoC projects and added spec for OCR 2019-12-16 16:47:06 +00:00
Laurent Cozic
07b175c2ee Revert "Desktop: Upgrade to Electron 5"
This reverts commit 37dbb81425.
2019-12-15 19:05:44 +00:00
Laurent Cozic
6132cf2128 Desktop, Cli: Allow exporting a note as HTML 2019-12-15 18:41:13 +00:00
Laurent Cozic
37dbb81425 Desktop: Upgrade to Electron 5 2019-12-14 23:43:34 +00:00
Laurent Cozic
c1028ec2cf Update website 2019-12-14 10:57:05 +00:00
Laurent Cozic
f98dc4e576 Update website 2019-12-14 10:56:13 +00:00
Laurent Cozic
da044960f9 Tools: Improved TypeScript config 2019-12-14 10:55:58 +00:00
Laurent Cozic
03522b48a5 CliClient: Fixed regression following recent PR 2019-12-14 10:55:42 +00:00
Laurent Cozic
1b31525773 Doc: Update Nextcloud app doc 2019-12-14 10:36:30 +00:00
Laurent Cozic
64a1408d6c Electron release v1.0.176 2019-12-14 01:28:50 +00:00
Laurent Cozic
3a5e68fca0 Minor changes for TypeScript 2019-12-14 01:28:37 +00:00
Laurent Cozic
48ce788118 Merge branch 'master' of github.com:laurent22/joplin 2019-12-13 22:38:39 +00:00
Laurent Cozic
34f0a2951a Desktop: Add ability to share a note publicly using Nextcloud (#2173)
* Moved button row to separate component file and started Sharing dialog

* Adding Sharing dialog

* Applied "npx react-codemod rename-unsafe-lifecycles"

* More UI

* Tools: Improved TypeScript integration

* Improved share dialog

* Tools Added support for translation validation in CI, and added support for plural translations

* Improved UI and sharing workflow

* Share workflow

* Cleaned up and improved sharing config error handling

* Fixed build scripts and doc for TypeScript

* Run linter
2019-12-13 01:16:34 +00:00
Laurent Cozic
66546418e3 Merge branch 'master' of github.com:laurent22/joplin 2019-12-13 00:53:57 +00:00
Devon Zuegel
611be7c0fa Desktop: Allow for custom Joplin theme and Ace editor styles (#2099)
* Delete unused file

* Implement CssUtils

* Inject custom CSS styles

* Add info about custom CSS styles to README

* Add note that ElectronClient/app/app.js is generated

* Add support for Setting.TYPE_BUTTON

* Add buttons in Preferences to open custom CSS files

* Swap custom CSS filenames

* Swap custom CSS filenames

* Wrap "Edit" with translation fn

* Incorporate PR feedback from @laurent22

* Add openOrCreateFile to Settings

* Move openOrCreateFile to shim

* Removing header for now - see https://github.com/laurent22/joplin/pull/2099#discussion_r353120915
2019-12-13 00:40:58 +00:00
Helmut K. C. Tessarek
4f3e031f4f Update website 2019-12-12 19:34:51 +01:00
Laurent Cozic
554c46182a Reverted sv translation update has it is invalid 2019-12-11 10:16:19 +00:00
Laurent Cozic
b5d5d02a9c Tools Added support for translation validation in CI, and added support for plural translations 2019-12-10 21:10:47 +00:00
Laurent Cozic
4640b65b85 finished renaming 2019-12-10 15:23:43 +00:00
Laurent Cozic
1615c6bdc8 renaming 2019-12-10 15:23:29 +00:00
Laurent Cozic
c003b8d32d iOS: Update application icon 2019-12-09 16:56:01 +00:00
Alexey
3a1f924fb1 Translation: Update ru_RU.po (#2164)
Filled untranslated string and correct some sentencies.
2019-12-09 06:38:57 -05:00
githubaccount073
583460c0a8 Translation: Update sv.po (#2163)
Added missing translations
Made adjustments to previous translations.
2019-12-09 06:38:03 -05:00
Laurent Cozic
1550a52002 iOS v10.0.41 2019-12-08 12:08:02 +00:00
Laurent Cozic
9bd3bc8404 Fixed clipper icons 2019-12-08 10:22:10 +00:00
352 changed files with 46821 additions and 11589 deletions

View File

@@ -45,4 +45,10 @@ Server/docs/
Server/dist/
Server/bin/
Server/node_modules/
ElectronClient/app/packageInfo.js
ElectronClient/app/packageInfo.js
ReactNativeClient/pluginAssets/
# Ignore files generated from TypeScript files
ElectronClient/app/gui/ShareNoteDialog.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/PluginAssetsLoader.js

6
.gitignore vendored
View File

@@ -44,3 +44,9 @@ ElectronClient/app/gui/note-viewer/fonts/
ElectronClient/app/gui/note-viewer/lib.js
Tools/commit_hook.txt
.vscode/*
*.map
# Ignore files generated from TypeScript files
ElectronClient/app/gui/ShareNoteDialog.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/PluginAssetsLoader.js

View File

@@ -50,12 +50,17 @@ before_install:
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update || true
sudo apt-get install -y yarn
sudo apt-get install -y gettext
fi
script:
- |
# Copy lib
rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
# Install tools
npm install
npm run tsc
cd Tools
npm install
cd ..
@@ -84,6 +89,19 @@ script:
fi
fi
# Validate translations - this is needed as some users manually
# edit .po files (and often make mistakes) instead of using a proper
# tool like poedit. Doing it for Linux only is sufficient.
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
if [ "$TRAVIS_OS_NAME" != "osx" ]; then
node Tools/validate-translation.js
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# Find out if we should run the build or not. Electron-builder gets stuck when
# builing PRs so we disable it in this case. The Linux build should provide
# enough info if the app builds or not.
@@ -96,5 +114,4 @@ script:
# Prepare the Electron app and build it
cd ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && USE_HARD_LINKS=false yarn dist

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg40"
version="1.1"
width="1536"
height="1536"
viewBox="0 0 1536 1536">
<metadata
id="metadata46">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs44" />
<path
id="path38"
fill="#ffffff"
d="M 373.834,0 C 168.227,0 0,168.223 0,373.834 V 1162.17 C 0,1367.778 168.227,1536 373.834,1536 H 1162.17 C 1367.778,1536 1536,1367.778 1536,1162.17 V 373.834 C 1536,168.224 1367.778,0 1162.17,0 Z m 397.222,205.431 h 417.424 a 7.132,7.132 0 0 1 7.132,7.133 v 132.552 c 0,4.461 -3.619,8.073 -8.077,8.073 h -57.23 c -24.168,0 -43.768,19.338 -44.284,43.374 v 2.377 h -0.017 v 136.191 h -0.053 l -0.466,509.375 c -5.02,77.667 -39.222,149.056 -96.324,201.046 -60.28,54.834 -141.948,85.017 -229.962,85.017 -12.45,0 -25.208,-0.61 -37.907,-1.785 -92.157,-8.682 -181.494,-48.601 -251.662,-112.438 -71.99,-65.517 -117.147,-150.03 -127.164,-238 -11.226,-98.763 23.42,-192.783 95.045,-257.937 81.99,-74.637 198.185,-101.768 316.613,-75.704 5.574,1.227 9.55,6.282 9.55,11.997 v 199.52 c -0.199,2.625 -1.481,6.599 -8.183,2.896 -0.663,-0.365 -1.194,-0.511 -1.653,-0.531 -21.987,-10.587 -45.159,-17.57 -68.559,-19.916 -0.38,-0.04 -0.757,-0.124 -1.138,-0.163 -0.537,-0.048 -1.034,-0.033 -1.556,-0.075 -4.13,-0.354 -8.183,-0.517 -12.203,-0.58 -0.87,-0.011 -1.771,-0.127 -2.641,-0.127 -0.486,0 -0.951,0.05 -1.437,0.057 -1.464,0.011 -2.886,0.115 -4.33,0.163 -2.76,0.102 -5.497,0.211 -8.182,0.448 -0.273,0.024 -0.547,0.07 -0.835,0.097 -25.509,2.4 -47.864,11.104 -65.012,25.47 -0.954,0.802 -1.974,1.53 -2.9,2.36 a 1.34,1.34 0 0 1 -0.168,0.146 c -23.96,21.8 -34.881,53.872 -30.726,90.316 4.62,40.737 26.94,81.156 62.841,113.823 35.908,32.67 80.335,52.977 125.113,57.186 35.118,3.36 66.547,-3.919 89.899,-20.461 a 97.255,97.255 0 0 0 9.365,-7.501 c 2.925,-2.661 5.569,-5.5 8.086,-8.416 0.3,-0.348 0.672,-0.673 0.975,-1.024 8.253,-9.864 14.222,-21.067 17.996,-33.148 0.639,-2.034 1.051,-4.148 1.564,-6.227 0.381,-1.563 0.81,-3.106 1.112,-4.693 0.555,-2.784 0.923,-5.632 1.253,-8.49 0.086,-0.709 0.183,-1.414 0.237,-2.128 0.492,-4.893 0.693,-9.858 0.55,-14.91 h 0.013 V 393.623 c -2.01,-22.626 -20.78,-40.434 -43.928,-40.434 h -57.23 a 8.071,8.071 0 0 1 -8.077,-8.073 V 212.564 a 7.132,7.132 0 0 1 7.136,-7.133 z" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -5,6 +5,14 @@
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
# TypeScript
Most of the application is written in JavaScript, however new classes and files should generally be written in [TypeScript](https://www.typescriptlang.org/). Even if you don't write TypeScript code, you will need to build the existing .ts and .tsx files. This is done from the root of the project, by running `npm run tsc`.
If you are modifying TypeScript code, the best is to have the compiler watch for changes from a terminal. To do so, run `npm run tsc-watch`.
All TypeScript files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. If you create a new TypeScript file, make sure you add the generated .js file to .gitignore. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
## macOS dependencies
brew install yarn node
@@ -14,7 +22,7 @@
## Linux and Windows (WSL) dependencies
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
- Install node v10.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
@@ -28,8 +36,9 @@ npm install && cd Tools && npm install
# Building the Electron application
```
npm run copyLib
npm run tsc
cd ElectronClient/app
rsync --delete -a ../../ReactNativeClient/lib/ lib/
npm install
yarn dist
```
@@ -47,10 +56,9 @@ From `/ElectronClient` you can also run `run.sh` to run the app for testing.
## Building Electron application on Windows
```
cd Tools
npm install
cd ..\ElectronClient\app
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
npm run tsc
cd ElectronClient\app
npm install
yarn dist
```
@@ -63,11 +71,36 @@ If you get an `error MSB8020: The build tools for v140 cannot be found.` try to
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
## Troubleshooting desktop application
> The application window doesn't open or is white
This is an indication that there's an early initialisation error. Try this:
- In ElectronAppWrapper, set `debugEarlyBugs` to `true`. This will force the window to show up and should open the console next to it, which should display any error.
- In more rare cases, an already open instance of Joplin can create strange low-level bugs that will display no error but will result in this white window. A non-dev instance of Joplin, or a dev instance that wasn't properly closed might cause this. So make sure you close everything and try again. Perhaps even other Electron apps running (Skype, Slack, etc.) could cause this?
- Also try to delete node_modules and rebuild.
- If all else fail, switch your computer off and on again, to make sure you start clean.
> How to work on the app from Windows?
You should not use WSL at all because this is a GUI app that lives outside of WSL, and the WSL layer can cause all kind of very hard to debug issues. It can also lock files in node_modules that cannot be unlocked when the app crashes (you need to restart your computer). Likewise, don't run the TypeScript watch command from WSL.
So everything should be done from a Windows Command prompt running as Administrator. You can use `run.bat` to run the app in dev mode.
# Building the Mobile application
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "React Native CLI Quickstart" tab.
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
Then:
```
npm run tsc
cd ReactNativeClient
npm install
react-native run-ios
# Or: react-native run-android
```
# Building the Terminal application
@@ -75,7 +108,6 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
cd CliClient
npm install
./build.sh
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
```
Run `run.sh` to start the application for testing.

View File

@@ -65,6 +65,12 @@ To run the test units, you must have an instance of the cli app running. In a fi
> rsync -aP ./ReactNativeClient/locales/ ./CliClient/build/locales/
> ```
> If you get an error like `Error: Cannot find module './path/to/node_sqlite3.node'`, that likely means you need to install sqlite:
>
> ```sh
> npm install sqlite3 --build-from-source
> ```
Then run the tests in a second window. To run all the test units:
./run_test.sh
@@ -72,3 +78,17 @@ Then run the tests in a second window. To run all the test units:
To run just one particular file:
./run_test.sh markdownUtils # Don't add the .js extension
To filter tests:
./run_test.sh "should handle conflict" # Will run all the test units that contain "should handle conflict" in their description
## About abandoned pull requests
It happens that a pull request is started but not finished and despite our attempts to contact the contributor, we don’t hear from them again.
In that case we will not merge the pull request, even if only small changes are missing. Our policy is simply to close the pull request. Why? Because an unfinished pull request essentially means giving us work and moving on. We would rather not encourage this behaviour.
Also, please note that since we have spent time reviewing the pull request and proposing solutions, we reserve the right to re-use that knowledge to create a new pull request, potentially based on your changes.
We’d much prefer that you complete the pull request though, so we’ll be sure to ping you a few times before that!

View File

@@ -131,6 +131,24 @@ class Command extends BaseCommand {
lines.push('');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching');
lines.push('');
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
lines.push('');
lines.push('For example, to retrieve the notebook named `recipes`: **GET /search?query=recipes&type=folder**');
lines.push('');
lines.push('To retrieve all the tags that start with `project-`: **GET /search?query=project-*&type=tag**');
lines.push('');
lines.push('# Item type IDs');
lines.push('');
lines.push('Item type IDs might be refered to in certain object you will retrieve from the API. This is the correspondance between name and ID:');
lines.push('');
lines.push('Name | Value');
lines.push('---- | -----');
for (const t of BaseModel.typeEnum_) {
const value = t[1];
lines.push(`${BaseModel.modelTypeToName(value)} | ${value} `);
}
lines.push('');
for (let i = 0; i < models.length; i++) {
const model = models[i];

View File

@@ -1,6 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { _, setLocale } = require('lib/locale.js');
const { app } = require('./app.js');
const fs = require('fs-extra');
const Setting = require('lib/models/Setting.js');
class Command extends BaseCommand {
@@ -13,11 +14,60 @@ class Command extends BaseCommand {
}
options() {
return [['-v, --verbose', _('Also displays unset and hidden config variables.')]];
return [
['-v, --verbose', _('Also displays unset and hidden config variables.')],
['--export', 'Writes all settings to STDOUT as JSON including secure variables.'],
['--import', 'Reads in JSON formatted settings from STDIN.'],
['--import-file <file>', 'Reads in settings from <file>. <file> must contain valid JSON.'],
];
}
async __importSettings(inputStream) {
return new Promise((resolve, reject) => {
// being defensive and not attempting to settle twice
let isSettled = false;
const chunks = [];
inputStream.on('readable', () => {
let chunk;
while ((chunk = inputStream.read()) !== null) {
chunks.push(chunk);
}
});
inputStream.on('end', () => {
let json = chunks.join('');
let settingsObj;
try {
settingsObj = JSON.parse(json);
} catch (err) {
isSettled = true;
return reject(new Error(`Invalid JSON passed to config --import: \n${err.message}.`));
}
if (settingsObj) {
Object.entries(settingsObj)
.forEach(([key, value]) => {
Setting.setValue(key, value);
});
}
if (!isSettled) {
isSettled = true;
resolve();
}
});
inputStream.on('error', (error) => {
if (!isSettled) {
isSettled = true;
reject(error);
}
});
});
}
async action(args) {
const verbose = args.options.verbose;
const isExport = args.options.export;
const isImport = args.options.import || args.options.importFile;
const importFile = args.options.importFile;
const renderKeyValue = name => {
const md = Setting.settingMetadata(name);
@@ -32,35 +82,45 @@ class Command extends BaseCommand {
}
};
if (!args.name && !args.value) {
if (isExport || (!isImport && !args.value)) {
let keys = Setting.keys(!verbose, 'cli');
keys.sort();
for (let i = 0; i < keys.length; i++) {
const value = Setting.value(keys[i]);
if (!verbose && !value) continue;
this.stdout(renderKeyValue(keys[i]));
if (isExport) {
const resultObj = keys.reduce((acc, key) => {
const value = Setting.value(key);
if (!verbose && !value) return acc;
acc[key] = value;
return acc;
}, {});
// Printing the object in "pretty" format so it's easy to read/edit
this.stdout(JSON.stringify(resultObj, null, 2));
} else if (!args.name) {
for (let i = 0; i < keys.length; i++) {
const value = Setting.value(keys[i]);
if (!verbose && !value) continue;
this.stdout(renderKeyValue(keys[i]));
}
} else {
this.stdout(renderKeyValue(args.name));
}
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
app().gui().showConsole();
app().gui().maximizeConsole();
return;
}
if (args.name && !args.value) {
this.stdout(renderKeyValue(args.name));
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
return;
if (isImport) {
let fileStream = process.stdin;
if (importFile) {
fileStream = fs.createReadStream(importFile, { autoClose: true });
}
await this.__importSettings(fileStream);
} else {
Setting.setValue(args.name, args.value);
}
Setting.setValue(args.name, args.value);
if (args.name == 'locale') {
setLocale(Setting.value('locale'));

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
const Setting = require('lib/models/Setting.js');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const { Synchronizer } = require('lib/synchronizer.js');

View File

@@ -8,8 +8,8 @@ require('app-module-path').addPath(__dirname);
const compareVersion = require('compare-version');
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
if (compareVersion(nodeVersion, '8.0.0') < 0) {
console.error(`Joplin requires Node 8+. Detected version ${nodeVersion}`);
if (compareVersion(nodeVersion, '10.0.0') < 0) {
console.error(`Joplin requires Node 10+. Detected version ${nodeVersion}`);
process.exit(1);
}

View File

@@ -7,4 +7,10 @@ rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
# Don't add TypeScript here or make it silent as output of Cli app must be clean
# cd $ROOT_DIR/..
# npm run tsc
# cd $ROOT_DIR
chmod 755 "$BUILD_DIR/main.js"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3152
CliClient/locales/pt_PT.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "1.0.149",
"version": "1.0.150",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -806,6 +806,11 @@
"debug": "^2.6.9"
}
},
"font-awesome-filetypes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/font-awesome-filetypes/-/font-awesome-filetypes-2.1.0.tgz",
"integrity": "sha512-U6hi14GRjfZFIWsTNyVmCBuHyPhiizWEKVbaQqHipKQv3rA1l1PNvmKulzpqxonFnQMToty5ZhfWbc/0IjLDGA=="
},
"for-each-property": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/for-each-property/-/for-each-property-0.0.4.tgz",
@@ -1131,9 +1136,9 @@
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"ignore-walk": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
@@ -1533,6 +1538,73 @@
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"joplin-renderer": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/joplin-renderer/-/joplin-renderer-1.0.8.tgz",
"integrity": "sha512-Q4SSYXl9ErcnUDTz7N4FjWcOOgfPEf5yyNRjU2J1fuxZ/1VbYt6MnfBB7OeiRW+XF+4Arhihk+/XVK++of4hEA==",
"requires": {
"base-64": "^0.1.0",
"font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0",
"highlight.js": "^9.17.1",
"html-entities": "^1.2.1",
"json-stringify-safe": "^5.0.1",
"katex": "^0.11.1",
"markdown-it": "^10.0.0",
"markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^5.2.5",
"markdown-it-deflist": "^2.0.3",
"markdown-it-emoji": "^1.4.0",
"markdown-it-footnote": "^3.0.2",
"markdown-it-ins": "^3.0.0",
"markdown-it-mark": "^3.0.0",
"markdown-it-multimd-table": "^4.0.1",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"uslug": "^1.0.4"
},
"dependencies": {
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
},
"highlight.js": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.0.tgz",
"integrity": "sha512-A97kI1KAUzKoAiEoaGcf2O9YPS8nbDTCRFokaaeBhnqjQTvbAuAJrQMm21zw8s8xzaMtCQBtgbyGXLGxdxQyqQ=="
},
"markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
}
}
},
"joplin-turndown": {
"version": "4.0.19",
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.19.tgz",
@@ -1544,9 +1616,9 @@
}
},
"joplin-turndown-plugin-gfm": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.11.tgz",
"integrity": "sha512-S2I+VCTqIhpWKKkPHsyJ5rdll9H/JjMXoBVClRX1TnphcmrSxufevdoXWWVgLncdXpSSiuoifCXgFZy3ueVElg=="
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA=="
},
"jpeg-js": {
"version": "0.1.2",
@@ -1636,6 +1708,21 @@
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"katex": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
"requires": {
"commander": "^2.19.0"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}
}
},
"klaw": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
@@ -1755,6 +1842,64 @@
"uc.micro": "^1.0.5"
}
},
"markdown-it-abbr": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
},
"markdown-it-anchor": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz",
"integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ=="
},
"markdown-it-deflist": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.0.3.tgz",
"integrity": "sha512-/BNZ8ksW42bflm1qQLnRI09oqU2847Z7MVavrR0MORyKLtiUYOMpwtlAfMSZAQU9UCvaUZMpgVAqoS3vpToJxw=="
},
"markdown-it-emoji": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
},
"markdown-it-footnote": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
},
"markdown-it-ins": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.0.tgz",
"integrity": "sha512-+vyAdBuMGwmT2yMlAFJSx2VR/0QZ1onQ/Mkkmr4l9tDFOh5sVoAgRbkgbuSsk+sxJ9vaMH/IQ323ydfvQrPO/Q=="
},
"markdown-it-mark": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz",
"integrity": "sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw=="
},
"markdown-it-multimd-table": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.0.1.tgz",
"integrity": "sha512-ZgRV8LlGz6JXTZ5zd82yCL8IVG5MRastMWxxrc6hQC8aC8kq/7zpp+ksBqVqcdTmTdabnkuSo/7h3SyKM31YCA==",
"requires": {
"markdown-it": "^8.4.2"
}
},
"markdown-it-sub": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
"integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g="
},
"markdown-it-sup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
},
"markdown-it-toc-done-right": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.1.0.tgz",
"integrity": "sha512-UhD2Oj6cZV3ycYPoelt4hTkwKIK3zbPP1wjjdpCq7UGtWQOFalDFDv1s2zBYV6aR2gMs/X8kpJcOYsQmUbiXDw=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -1859,9 +2004,9 @@
}
},
"nan": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"napi-build-utils": {
"version": "1.0.1",
@@ -1887,27 +2032,27 @@
}
},
"needle": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz",
"integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
"requires": {
"debug": "^4.1.0",
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
@@ -1996,14 +2141,22 @@
}
},
"npm-bundled": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz",
"integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1"
@@ -2226,7 +2379,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
@@ -2676,137 +2829,13 @@
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
},
"sqlite3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.7.tgz",
"integrity": "sha512-TGEeSBB8O48bEu8KUUMqzeB22WrfTxzhIf0lFm8wLTo3a6yJBonF2sPKMYrYtOne1F1t9AHAEn+DTISq8WebQg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
"integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
"requires": {
"nan": "^2.12.1",
"node-pre-gyp": "^0.11.0",
"request": "^2.87.0"
},
"dependencies": {
"ajv": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
}
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
},
"sshpk": {
@@ -3205,6 +3234,11 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"unorm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
},
"unpack-string": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz",
@@ -3244,6 +3278,14 @@
"requires-port": "^1.0.0"
}
},
"uslug": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
"requires": {
"unorm": ">= 1.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -25,7 +25,7 @@
"joplin": "./main.js"
},
"engines": {
"node": ">=8.7.0"
"node": ">=10.0.0"
},
"dependencies": {
"app-module-path": "^2.2.0",
@@ -44,8 +44,9 @@
"html-minifier": "^3.5.15",
"image-data-uri": "^2.0.0",
"image-type": "^3.0.0",
"joplin-renderer": "^1.0.8",
"joplin-turndown": "^4.0.19",
"joplin-turndown-plugin-gfm": "^1.0.11",
"joplin-turndown-plugin-gfm": "^1.0.12",
"jssha": "^2.3.0",
"levenshtein": "^1.0.5",
"markdown-it": "^8.4.2",
@@ -66,7 +67,7 @@
"server-destroy": "^1.0.1",
"sharp": "^0.23.2",
"sprintf-js": "^1.1.1",
"sqlite3": "^4.0.7",
"sqlite3": "^4.1.1",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",

41
CliClient/tests/MdToMd.js Normal file
View File

@@ -0,0 +1,41 @@
const mdImporterService = require('lib/services/InteropService_Importer_Md');
const Note = require('lib/models/Note.js');
const { setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
const importer = new mdImporterService();
describe('InteropService_Importer_Md: importLocalImages', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should import linked files and modify tags appropriately', async function() {
const tagNonExistentFile = '![does not exist](does_not_exist.png)';
const note = await importer.importFile(`${__dirname}/md_to_md/sample.md`, 'notebook');
let items = await Note.linkedItems(note.body);
expect(items.length).toBe(2);
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
expect(inexistentLinkUnchanged).toBe(true);
});
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-duplicate-links.md`, 'notebook');
let items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
const reg = new RegExp(items[0].id, 'g');
const matched = note.body.match(reg);
expect(matched.length).toBe(2);
});
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-link-in-alt-text.md`, 'notebook');
let items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should passthrough unchanged if no links present', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-no-links.md`, 'notebook');
let items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
});
});

View File

@@ -41,7 +41,7 @@ describe('Encryption', function() {
};
const encodedHeader = service.encodeHeader_(header);
const decodedHeader = service.decodeHeader_(encodedHeader);
const decodedHeader = service.decodeHeaderBytes_(encodedHeader);
delete decodedHeader.length;
expect(objectsEqual(header, decodedHeader)).toBe(true);
@@ -54,14 +54,14 @@ describe('Encryption', function() {
let hasThrown = false;
try {
await service.decryptMasterKey(masterKey, 'wrongpassword');
await service.decryptMasterKey_(masterKey, 'wrongpassword');
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
const decryptedMasterKey = await service.decryptMasterKey_(masterKey, '123456');
expect(decryptedMasterKey.length).toBe(512);
}));
@@ -69,7 +69,7 @@ describe('Encryption', function() {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
const plainText = await service.decryptString(cipherText);
@@ -87,11 +87,37 @@ describe('Encryption', function() {
expect(plainText2 === veryLongSecret).toBe(true);
}));
it('should decrypt various encryption methods', asyncTest(async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey_(masterKey, '123456', true);
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_2);
}
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionService.METHOD_SJCL_3,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_3);
}
}));
it('should fail to decrypt if master key not present', asyncTest(async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
@@ -107,7 +133,7 @@ describe('Encryption', function() {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
let cipherText = await service.encryptString('some secret');
cipherText += 'ABCDEFGHIJ';
@@ -120,7 +146,7 @@ describe('Encryption', function() {
it('should encrypt and decrypt notes and folders', asyncTest(async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
let folder = await Folder.save({ title: 'folder' });
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
@@ -151,7 +177,7 @@ describe('Encryption', function() {
it('should encrypt and decrypt files', asyncTest(async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
const sourcePath = `${__dirname}/../tests/support/photo.jpg`;
const encryptedPath = `${__dirname}/data/photo.crypted`;
@@ -164,4 +190,34 @@ describe('Encryption', function() {
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
}));
// it('should upgrade master key encryption mode', asyncTest(async () => {
// let masterKey = await service.generateMasterKey('123456', {
// encryptionMethod: EncryptionService.METHOD_SJCL_2,
// });
// masterKey = await MasterKey.save(masterKey);
// Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
// Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
// await sleep(0.01);
// await service.loadMasterKeysFromSettings();
// masterKeyNew = await MasterKey.load(masterKey.id);
// // Check that the master key has been upgraded
// expect(masterKeyNew.created_time).toBe(masterKey.created_time);
// expect(masterKeyNew.updated_time === masterKey.updated_time).toBe(false);
// expect(masterKeyNew.content === masterKey.content).toBe(false);
// expect(masterKeyNew.encryption_method === masterKey.encryption_method).toBe(false);
// expect(masterKeyNew.checksum === masterKey.checksum).toBe(false);
// expect(masterKeyNew.encryption_method).toBe(service.defaultMasterKeyEncryptionMethod_);
// // Check that encryption still works
// const cipherText = await service.encryptString('some secret');
// const plainText = await service.decryptString(cipherText);
// expect(plainText).toBe('some secret');
// }));
});

View File

@@ -0,0 +1,2 @@
![link 1](../support/photo.jpg)
![link 2](../support/photo.jpg)

View File

@@ -0,0 +1,3 @@
# Markdown
![../support/photo.jpg](../support/photo.jpg) should put resource link inside () not []
![../support/photo.jpg]( ../support/photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View File

@@ -0,0 +1,3 @@
# Markdown
Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.

View File

@@ -0,0 +1,13 @@
# Markdown
lorem ipsum ![alt text here](../support/photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../support/photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../support) lorem ipsum

View File

@@ -150,4 +150,31 @@ describe('models_Folder', function() {
expect(foldersById[f4.id].note_count).toBe(0);
}));
it('should not count completed to-dos', asyncTest(async () => {
let f1 = await Folder.save({ title: 'folder1' });
let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
let f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
let f4 = await Folder.save({ title: 'folder4' });
let n1 = await Note.save({ title: 'note1', parent_id: f3.id });
let n2 = await Note.save({ title: 'note2', parent_id: f3.id });
let n3 = await Note.save({ title: 'note3', parent_id: f1.id });
let n4 = await Note.save({ title: 'note4', parent_id: f3.id, is_todo: true, todo_completed: 0 });
let n5 = await Note.save({ title: 'note5', parent_id: f3.id, is_todo: true, todo_completed: 999 });
let n6 = await Note.save({ title: 'note6', parent_id: f3.id, is_todo: true, todo_completed: 999 });
const folders = await Folder.all();
await Folder.addNoteCounts(folders, false);
const foldersById = {};
folders.forEach((f) => { foldersById[f.id] = f; });
expect(folders.length).toBe(4);
expect(foldersById[f1.id].note_count).toBe(4);
expect(foldersById[f2.id].note_count).toBe(3);
expect(foldersById[f3.id].note_count).toBe(3);
expect(foldersById[f4.id].note_count).toBe(0);
}));
});

350
CliClient/tests/reducer.js Normal file
View File

@@ -0,0 +1,350 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const {setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { reducer, defaultState, stateUtils} = require('lib/reducer.js');
async function createNTestFolders(n) {
let folders = [];
for (let i = 0; i < n; i++) {
let folder = await Folder.save({ title: 'folder' });
folders.push(folder);
}
return folders;
}
async function createNTestNotes(n, folder) {
let notes = [];
for (let i = 0; i < n; i++) {
let note = await Note.save({ title: 'note', parent_id: folder.id });
notes.push(note);
}
return notes;
}
async function createNTestTags(n) {
let tags = [];
for (let i = 0; i < n; i++) {
let tag = await Tag.save({ title: 'tag' });
tags.push(tag);
}
return tags;
}
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
let state = defaultState;
if (folders != null) {
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
}
if (selectedFolderIndex != null) {
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
}
if (notes != null) {
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
}
if (selectedIndexes != null) {
let selectedIds = [];
for (let i = 0; i < selectedIndexes.length; i++) {
selectedIds.push(notes[selectedIndexes[i]].id);
}
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
}
if (tags != null) {
state = reducer(state, { type: 'TAG_UPDATE_ALL', items: tags });
}
if (selectedTagIndex != null) {
state = reducer(state, { type: 'TAG_SELECT', id: tags[selectedTagIndex].id });
}
return state;
}
function createExpectedState(items, keepIndexes, selectedIndexes) {
let expected = { items: [], selectedIds: []};
for (let i = 0; i < selectedIndexes.length; i++) {
expected.selectedIds.push(items[selectedIndexes[i]].id);
}
for (let i = 0; i < keepIndexes.length; i++) {
expected.items.push(items[keepIndexes[i]]);
}
return expected;
}
function getIds(items, indexes=null) {
let ids = [];
for (let i = 0; i < items.length; i++) {
if (indexes == null || i in indexes) {
ids.push(items[i].id);
}
}
return ids;
}
let insideBeforeEach = false;
describe('Reducer', function() {
beforeEach(async (done) => {
insideBeforeEach = true;
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
insideBeforeEach = false;
});
// tests for NOTE_DELETE
it('should delete selected note', asyncTest(async () => {
// create 1 folder
let folders = await createNTestFolders(1);
// create 5 notes
let notes = await createNTestNotes(5, folders[0]);
// select the 1st folder and the 3rd note
let state = initTestState(folders, 0, notes, [2]);
// test action
// delete the third note
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
// expect that the third note is missing, and the 4th note is now selected
let expected = createExpectedState(notes, [0,1,3,4], [3]);
// check the ids of all the remaining notes
expect(getIds(state.notes)).toEqual(getIds(expected.items));
// check the ids of the selected notes
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected note at top', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
let expected = createExpectedState(notes, [1,2,3,4], [1]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete last remaining note', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(1, folders[0]);
let state = initTestState(folders, 0, notes, [0]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
let expected = createExpectedState(notes, [], []);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected note at bottom', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [4]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
let expected = createExpectedState(notes, [0,1,2,3], [3]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a note below is selected', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
let expected = createExpectedState(notes, [0,2,3,4], [3]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a note above is selected', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
let expected = createExpectedState(notes, [0,1,2,4], [1]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected notes', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1,2]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
let expected = createExpectedState(notes, [0,3,4], [3]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a notes below it are selected', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3,4]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
let expected = createExpectedState(notes, [0,2,3,4], [3,4]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a notes above it are selected', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1,2]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
let expected = createExpectedState(notes, [0,1,2,4], [1,2]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete notes at end', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3,4]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
let expected = createExpectedState(notes, [0,1,2], [2]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete notes when non-contiguous selection', asyncTest(async () => {
let folders = await createNTestFolders(1);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [0,2,4]);
// test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
let expected = createExpectedState(notes, [1,3], [1]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
// tests for FOLDER_DELETE
it('should delete selected notebook', asyncTest(async () => {
let folders = await createNTestFolders(5);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 2, notes, [2]);
// test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
let expected = createExpectedState(folders, [0,1,3,4], [3]);
expect(getIds(state.folders)).toEqual(getIds(expected.items));
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
}));
it('should delete notebook when a book above is selected', asyncTest(async () => {
let folders = await createNTestFolders(5);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 1, notes, [2]);
// test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
let expected = createExpectedState(folders, [0,1,3,4], [1]);
expect(getIds(state.folders)).toEqual(getIds(expected.items));
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
}));
it('should delete notebook when a book below is selected', asyncTest(async () => {
let folders = await createNTestFolders(5);
let notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 4, notes, [2]);
// test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
let expected = createExpectedState(folders, [0,1,3,4], [4]);
expect(getIds(state.folders)).toEqual(getIds(expected.items));
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
}));
// tests for TAG_DELETE
it('should delete selected tag', asyncTest(async () => {
let tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
// test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[2].id});
let expected = createExpectedState(tags, [0,1,3,4], [3]);
expect(getIds(state.tags)).toEqual(getIds(expected.items));
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
it('should delete tag when a tag above is selected', asyncTest(async () => {
let tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
// test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[4].id});
let expected = createExpectedState(tags, [0,1,2,3], [2]);
expect(getIds(state.tags)).toEqual(getIds(expected.items));
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
it('should delete tag when a tag below is selected', asyncTest(async () => {
let tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
// test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[0].id});
let expected = createExpectedState(tags, [1,2,3,4], [2]);
expect(getIds(state.tags)).toEqual(getIds(expected.items));
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
});

View File

@@ -0,0 +1,336 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const fs = require('fs-extra');
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
const InteropService_Exporter_Md = require('lib/services/InteropService_Exporter_Md.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');
const Note = require('lib/models/Note.js');
const { shim } = require('lib/shim.js');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
const exportDir = `${__dirname}/export`;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('services_InteropService_Exporter_Md', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await fs.remove(exportDir);
await fs.mkdirp(exportDir);
done();
});
it('should create resources directory', asyncTest(async () => {
const service = new InteropService_Exporter_Md();
await service.init(exportDir);
expect(await shim.fsDriver().exists(`${exportDir}/_resources/`)).toBe(true);
}));
it('should create note paths and add them to context', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
note1 = await Note.load(note1.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note2);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
let folder2 = await Folder.save({ title: 'folder2' });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${__dirname}/../tests/support/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false, 'Context should be empty before processing.');
await exporter.processItem(Folder, folder1);
await exporter.processItem(Folder, folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
}));
it('should handle duplicate note names', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
let note1_2 = await Note.save({ title: 'note1', parent_id: folder1.id });
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note1_2);
await exporter.processItem(Folder, folder1);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
}));
it('should not override existing files', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
await exporter.processItem(Folder, folder1);
// Create a file with the path of note1 before processing note1
await shim.fsDriver().writeFile(`${exportDir}/folder1/note1.md`, 'Note content', 'utf-8');
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
}));
it('should save resource files in _resource directory', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
note1 = await Note.load(note1.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
let resource1 = await Resource.load(itemsToExport[2].itemOrId);
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
note2 = await Note.load(note2.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
queueExportItem(BaseModel.TYPE_NOTE, note2);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note2.body))[0]);
let resource2 = await Resource.load(itemsToExport[5].itemOrId);
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
}));
it('should save notes in fs', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
queueExportItem(BaseModel.TYPE_NOTE, note2);
let folder3 = await Folder.save({ title: 'folder3' });
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
queueExportItem(BaseModel.TYPE_FOLDER, folder3.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
await exporter.processItem(Folder, folder1);
await exporter.processItem(Folder, folder2);
await exporter.processItem(Folder, folder3);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processItem(Note, note1);
await exporter.processItem(Note, note2);
await exporter.processItem(Note, note3);
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note1.id]}`)).toBe(true, 'File should be saved in filesystem.');
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note2.id]}`)).toBe(true, 'File should be saved in filesystem.');
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
}));
it('should replace resource ids with relative paths', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
note1 = await Note.load(note1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
let resource1 = await Resource.load((await Note.linkedResourceIds(note1.body))[0]);
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
note2 = await Note.load(note2.id);
queueExportItem(BaseModel.TYPE_NOTE, note2);
let resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
await exporter.processItem(Folder, folder1);
await exporter.processItem(Folder, folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
let context = {
resourcePaths: {},
};
context.resourcePaths[resource1.id] = 'resource1.jpg';
context.resourcePaths[resource2.id] = 'resource2.jpg';
exporter.updateContext(context);
await exporter.processItem(Note, note1);
await exporter.processItem(Note, note2);
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
}));
it('should replace note ids with relative paths', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
const changeNoteBodyAndReload = async (note, newBody) => {
note.body = newBody;
await Note.save(note);
return await Note.load(note.id);
};
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
let folder3 = await Folder.save({ title: 'folder3' });
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
note1 = await changeNoteBodyAndReload(note1, `# Some text \n\n [A link to note3](:/${note3.id})`);
note2 = await changeNoteBodyAndReload(note2, `# Some text \n\n [A link to note3](:/${note3.id}) some more text \n ## And some headers \n and [A link to note1](:/${note1.id}) more links`);
note3 = await changeNoteBodyAndReload(note3, `[A link to note3](:/${note2.id})`);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note2);
queueExportItem(BaseModel.TYPE_NOTE, note3);
await exporter.processItem(Folder, folder1);
await exporter.processItem(Folder, folder2);
await exporter.processItem(Folder, folder3);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processItem(Note, note1);
await exporter.processItem(Note, note2);
await exporter.processItem(Note, note3);
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
let note3_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note3.id]}`);
expect(note1_body).toContain('](../folder3/note3.md)', 'Note id should be replaced with a relative path.');
expect(note2_body).toContain('](../../folder3/note3.md)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../folder1/note1.md)', 'Resource id should be replaced with a relative path.');
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
}));
it('should url encode relative note links', asyncTest(async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
let folder1 = await Folder.save({ title: 'folder with space1' });
let note1 = await Note.save({ title: 'note1 name with space', parent_id: folder1.id });
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id, body: `[link](:/${note1.id})` });
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note2);
await exporter.processItem(Folder, folder1);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processItem(Note, note1);
await exporter.processItem(Note, note2);
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)', 'Whitespace in URL should be encoded');
}));
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -539,7 +539,7 @@ describe('Synchronizer', function() {
let context2 = await synchronizer().start();
if (withEncryption) {
const masterKey_2 = await MasterKey.load(masterKey.id);
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
let t = await Tag.load(tag.id);
await Tag.decrypt(t);
}
@@ -743,7 +743,7 @@ describe('Synchronizer', function() {
expect(masterKey_2.content).toBe(masterKey.content);
expect(masterKey_2.checksum).toBe(masterKey.checksum);
// Now load the master key we got from client 1 and try to decrypt
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
// Get the decrypted items back
await Folder.decrypt(folder1_2);
await Note.decrypt(note1_2);
@@ -1540,5 +1540,43 @@ describe('Synchronizer', function() {
expect((await synchronizer().lockFiles_()).length).toBe(0);
}));
it('should not encrypt notes that are shared', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'un', parent_id: folder1.id });
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await switchClient(1);
const origNote2 = Object.assign({}, note2);
await BaseItem.updateShareStatus(note2, true);
note2 = await Note.load(note2.id);
// Sharing a note should not modify the timestamps
expect(note2.user_updated_time).toBe(origNote2.user_updated_time);
expect(note2.user_created_time).toBe(origNote2.user_created_time);
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
// The shared note should be decrypted
let note2_2 = await Note.load(note2.id);
expect(note2_2.title).toBe('deux');
expect(note2_2.is_shared).toBe(1);
// The non-shared note should be encrypted
let note1_2 = await Note.load(note1.id);
expect(note1_2.title).toBe('');
}));
});

View File

@@ -299,7 +299,7 @@ async function loadEncryptionMasterKey(id = null, useExisting = false) {
masterKey = masterKeys[0];
}
await service.loadMasterKey(masterKey, '123456', true);
await service.loadMasterKey_(masterKey, '123456', true);
return masterKey;
}
@@ -370,8 +370,12 @@ function asyncTest(callback) {
try {
await callback();
} catch (error) {
console.error(error);
expect('good').toBe('not good', 'Test has thrown an exception - see above error');
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
// OK - will be reported by Jasmine
} else {
console.error(error);
expect(0).toBe(1, 'Test has thrown an exception - see above error');
}
} finally {
done();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -33,7 +33,14 @@ class ElectronAppWrapper {
return this.win_;
}
env() {
return this.env_;
}
createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' && false;
const windowStateKeeper = require('electron-window-state');
const stateOptions = {
@@ -56,9 +63,10 @@ class ElectronAppWrapper {
webPreferences: {
nodeIntegration: true,
},
webviewTag: true,
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
// https://github.com/laurent22/joplin/issues/2031
show: false,
show: debugEarlyBugs,
};
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
@@ -81,8 +89,11 @@ class ElectronAppWrapper {
slashes: true,
}));
// Uncomment this to view errors if the application does not start
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
// Note that on Windows, calling openDevTools() too early results in a white window with no error message.
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
// to this line though.
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
this.win_.on('close', (event) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)

View File

@@ -1,9 +1,97 @@
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const InteropService = require('lib/services/InteropService');
const Setting = require('lib/models/Setting');
const md5 = require('md5');
const url = require('url');
const { shim } = require('lib/shim');
class InteropServiceHelper {
static async exportNoteToHtmlFile(noteId, exportOptions) {
const tempFile = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.html`;
exportOptions = Object.assign({}, {
path: tempFile,
format: 'html',
target: 'file',
sourceNoteIds: [noteId],
customCss: '',
}, exportOptions);
const service = new InteropService();
const result = await service.export(exportOptions);
console.info('Export HTML result: ', result);
return tempFile;
}
static async exportNoteTo_(target, noteId, options = {}) {
let win = null;
let htmlFile = null;
const cleanup = () => {
if (win) win.destroy();
if (htmlFile) shim.fsDriver().remove(htmlFile);
};
try {
const exportOptions = {
customCss: options.customCss ? options.customCss : '',
};
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
const windowOptions = {
show: false,
};
win = bridge().newBrowserWindow(windowOptions);
return new Promise((resolve, reject) => {
win.webContents.on('did-finish-load', async () => {
if (target === 'pdf') {
try {
const data = await win.webContents.printToPDF(options);
resolve(data);
} catch (error) {
reject(error);
} finally {
cleanup();
}
} else {
win.webContents.print(options, (success) => {
// TODO: This is correct but broken in Electron 4. Need to upgrade to 5+
// It calls the callback right away with "false" even if the document hasn't be print yet.
cleanup();
if (!success) reject(new Error('Could not print'));
resolve();
});
}
});
win.loadURL(url.format({
pathname: htmlFile,
protocol: 'file:',
slashes: true,
}));
});
} catch (error) {
cleanup();
throw error;
}
}
static async exportNoteToPdf(noteId, options = {}) {
return this.exportNoteTo_('pdf', noteId, options);
}
static async printNote(noteId, options = {}) {
return this.exportNoteTo_('printer', noteId, options);
}
static async export(dispatch, module, options = null) {
if (!options) options = {};
@@ -32,6 +120,8 @@ class InteropServiceHelper {
const exportOptions = {};
exportOptions.path = path;
exportOptions.format = module.format;
exportOptions.modulePath = module.path;
exportOptions.target = module.target;
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;
@@ -41,6 +131,7 @@ class InteropServiceHelper {
const result = await service.export(exportOptions);
console.info('Export result: ', result);
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
}

View File

@@ -6,6 +6,7 @@ const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim.js');
const MasterKey = require('lib/models/MasterKey');
const Note = require('lib/models/Note');
const { MarkupToHtml } = require('joplin-renderer');
const { _, setLocale } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const fs = require('fs-extra');
@@ -28,6 +29,7 @@ const PluginManager = require('lib/services/PluginManager');
const RevisionService = require('lib/services/RevisionService');
const MigrationService = require('lib/services/MigrationService');
const TemplateUtils = require('lib/TemplateUtils');
const CssUtils = require('lib/CssUtils');
const pluginClasses = [
require('./plugins/GotoAnything.min'),
@@ -48,7 +50,7 @@ const appDefaultState = Object.assign({}, defaultState, {
windowContentSize: bridge().windowContentSize(),
watchedNoteFiles: [],
lastEditorScrollPercents: {},
noteDevToolsVisible: false,
devToolsVisible: false,
});
class Application extends BaseApplication {
@@ -220,7 +222,12 @@ class Application extends BaseApplication {
case 'NOTE_DEVTOOLS_TOGGLE':
newState = Object.assign({}, state);
newState.noteDevToolsVisible = !newState.noteDevToolsVisible;
newState.devToolsVisible = !newState.devToolsVisible;
break;
case 'NOTE_DEVTOOLS_SET':
newState = Object.assign({}, state);
newState.devToolsVisible = action.value;
break;
}
@@ -232,6 +239,14 @@ class Application extends BaseApplication {
return super.reducer(newState, action);
}
toggleDevTools(visible) {
if (visible) {
bridge().openDevTools();
} else {
bridge().closeDevTools();
}
}
async generalMiddleware(store, next, action) {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
@@ -273,12 +288,12 @@ class Application extends BaseApplication {
}
if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0) {
this.updateMenuItemStates();
this.updateMenuItemStates(newState);
}
if (action.type === 'NOTE_DEVTOOLS_TOGGLE') {
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
menuItem.checked = newState.noteDevToolsVisible;
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
this.toggleDevTools(newState.devToolsVisible);
this.updateMenuItemStates(newState);
}
return result;
@@ -371,13 +386,15 @@ class Application extends BaseApplication {
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type === 'exporter') {
exportItems.push({
label: module.fullLabel(),
screens: ['Main'],
click: async () => {
await InteropServiceHelper.export(this.dispatch.bind(this), module);
},
});
if (module.canDoMultiExport !== false) {
exportItems.push({
label: module.fullLabel(),
screens: ['Main'],
click: async () => {
await InteropServiceHelper.export(this.dispatch.bind(this), module);
},
});
}
} else {
for (let j = 0; j < module.sources.length; j++) {
const moduleSource = module.sources[j];
@@ -445,6 +462,7 @@ class Application extends BaseApplication {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'exportPdf',
noteId: null,
});
},
});
@@ -1104,18 +1122,23 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = screen;
}
async updateMenuItemStates() {
async updateMenuItemStates(state = null) {
if (!this.lastMenuScreen_) return;
if (!this.store()) return;
if (!this.store() && !state) return;
const selectedNoteIds = this.store().getState().selectedNoteIds;
if (!state) state = this.store().getState();
const selectedNoteIds = state.selectedNoteIds;
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) {
const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`);
if (!menuItem) continue;
menuItem.enabled = !!note && note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN;
menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
}
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
menuItem.checked = state.devToolsVisible;
}
updateTray() {
@@ -1167,6 +1190,39 @@ class Application extends BaseApplication {
return cssString;
}
// async createManyNotes() {
// return;
// const folderIds = [];
// const randomFolderId = (folderIds) => {
// if (!folderIds.length) return '';
// const idx = Math.floor(Math.random() * folderIds.length);
// if (idx > folderIds.length - 1) throw new Error('Invalid index ' + idx + ' / ' + folderIds.length);
// return folderIds[idx];
// }
// let rootFolderCount = 0;
// let folderCount = 100;
// for (let i = 0; i < folderCount; i++) {
// let parentId = '';
// if (Math.random() >= 0.9 || rootFolderCount >= folderCount / 10) {
// parentId = randomFolderId(folderIds);
// } else {
// rootFolderCount++;
// }
// const folder = await Folder.save({ title: 'folder' + i, parent_id: parentId });
// folderIds.push(folder.id);
// }
// for (let i = 0; i < 10000; i++) {
// const parentId = randomFolderId(folderIds);
// Note.save({ title: 'note' + i, parent_id: parentId });
// }
// }
async start(argv) {
const electronIsDev = require('electron-is-dev');
@@ -1176,13 +1232,18 @@ class Application extends BaseApplication {
argv = await super.start(argv);
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
const dir = Setting.value('profileDir');
const filename = Setting.custom_css_files.JOPLIN_APP;
await CssUtils.injectCustomStyles(`${dir}/${filename}`);
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
AlarmService.setLogger(reg.logger());
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message); });
if (Setting.value('openDevTools')) {
bridge().window().webContents.openDevTools();
if (Setting.value('flagOpenDevTools')) {
bridge().openDevTools();
}
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
@@ -1224,8 +1285,8 @@ class Application extends BaseApplication {
ids: Setting.value('collapsedFolderIds'),
});
const cssString = await this.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
// Loads custom Markdown preview styles
const cssString = await CssUtils.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
this.store().dispatch({
type: 'LOAD_CUSTOM_CSS',
css: cssString,
@@ -1238,6 +1299,11 @@ class Application extends BaseApplication {
templates: templates,
});
this.store().dispatch({
type: 'NOTE_DEVTOOLS_SET',
value: Setting.value('flagOpenDevTools'),
});
// Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {

View File

@@ -1,5 +1,6 @@
const { _, setLocale } = require('lib/locale.js');
const { dirname } = require('lib/path-utils.js');
const { BrowserWindow } = require('electron');
class Bridge {
@@ -13,6 +14,10 @@ class Bridge {
return this.electronWrapper_;
}
env() {
return this.electronWrapper_.env();
}
processArgv() {
return process.argv;
}
@@ -21,6 +26,10 @@ class Bridge {
return this.electronWrapper_.window();
}
newBrowserWindow(options) {
return new BrowserWindow(options);
}
windowContentSize() {
if (!this.window()) return { width: 0, height: 0 };
const s = this.window().getContentSize();
@@ -38,11 +47,19 @@ class Bridge {
return this.window().setSize(width, height);
}
openDevTools() {
return this.window().webContents.openDevTools();
}
closeDevTools() {
return this.window().webContents.closeDevTools();
}
showSaveDialog(options) {
const {dialog} = require('electron');
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
const filePath = dialog.showSaveDialog(this.window(), options);
const filePath = dialog.showSaveDialogSync(this.window(), options);
if (filePath) {
this.lastSelectedPath_ = filePath;
}
@@ -54,7 +71,7 @@ class Bridge {
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
if (!('createDirectory' in options)) options.createDirectory = true;
const filePaths = dialog.showOpenDialog(this.window(), options);
const filePaths = dialog.showOpenDialogSync(this.window(), options);
if (filePaths && filePaths.length) {
this.lastSelectedPath_ = dirname(filePaths[0]);
}
@@ -65,7 +82,7 @@ class Bridge {
showMessageBox_(window, options) {
const {dialog} = require('electron');
if (!window) window = this.window();
return dialog.showMessageBox(window, options);
return dialog.showMessageBoxSync(window, options);
}
showErrorMessageBox(message) {
@@ -126,6 +143,10 @@ class Bridge {
return this.electronApp().buildDir();
}
screen() {
return require('electron').screen;
}
}
let bridge_ = null;

View File

@@ -126,13 +126,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
autoUpdateLogger_.info(`checkForUpdates: Checking with options ${JSON.stringify(options)}`);
fetchLatestRelease(options).then(release => {
fetchLatestRelease(options).then(async (release) => {
autoUpdateLogger_.info(`Current version: ${packageInfo.version}`);
autoUpdateLogger_.info(`Latest version: ${release.version}`);
autoUpdateLogger_.info('Is Pre-release:', release.prerelease);
if (compareVersions(release.version, packageInfo.version) <= 0) {
if (!checkInBackground_) dialog.showMessageBox({
if (!checkInBackground_) await dialog.showMessageBox({
type: 'info',
message: _('Current version is up-to-date.'),
buttons: [_('OK')],
@@ -145,12 +145,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version;
const buttonIndex = dialog.showMessageBox(parentWindow_, {
const result = await dialog.showMessageBox(parentWindow_, {
type: 'info',
message: `${_('An update is available, do you want to download it now?')}\n\n${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`,
buttons: [_('Yes'), _('No')].concat(truncateReleaseNotes ? [_('Full Release Notes')] : []),
});
const buttonIndex = result.response;
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
if (buttonIndex === 2) require('electron').shell.openExternal(release.pageUrl);
}

View File

@@ -24,6 +24,19 @@ class ConfigScreenComponent extends React.Component {
await shared.checkSyncConfig(this, this.state.settings);
};
this.checkNextcloudAppButton_click = async () => {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
};
this.showLogButton_click = () => {
this.setState({ showNextcloudAppLog: true });
};
this.nextcloudAppHelpLink_click = () => {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
};
this.rowStyle_ = {
marginBottom: 10,
};
@@ -31,7 +44,7 @@ class ConfigScreenComponent extends React.Component {
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({ settings: this.props.settings });
}
@@ -93,14 +106,21 @@ class ConfigScreenComponent extends React.Component {
sectionToComponent(key, section, settings, selected) {
const theme = themeStyle(this.props.theme);
const settingComps = [];
// const settingComps = [];
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
const createSettingComponents = (advanced) => {
const output = [];
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
if (!!md.advanced !== advanced) continue;
const settingComp = this.settingToComponent(md.key, settings[md.key]);
output.push(settingComp);
}
return output;
};
const settingComp = this.settingToComponent(md.key, settings[md.key]);
settingComps.push(settingComp);
}
const settingComps = createSettingComponents(false);
const advancedSettingComps = createSettingComponents(true);
const sectionStyle = {
marginTop: 20,
@@ -117,10 +137,10 @@ class ConfigScreenComponent extends React.Component {
if (section.name === 'sync') {
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
{messages[0]}
@@ -137,12 +157,69 @@ class ConfigScreenComponent extends React.Component {
</div>
);
}
if (syncTargetMd.name === 'nextcloud') {
const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
let status = _('Unknown');
let errorMessage = null;
if (this.state.checkNextcloudAppResult === 'checking') {
status = _('Checking...');
} else if (syncTarget) {
if (syncTarget.uuid) status = _('OK');
if (syncTarget.error) {
status = _('Error');
errorMessage = syncTarget.error;
}
}
const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
<div style={statusStyle}>
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
<pre>{errorMessage}</pre>
</div>
);
const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
);
const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
settingComps.push(
<div key="nextcloud_app_check" style={this.rowStyle_}>
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
{_('Check Status')}
</button>
&nbsp;&nbsp;
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
{statusComp}
</div>
);
}
}
let advancedSettingsButton = null;
let advancedSettingsSectionStyle = { display: 'none' };
if (advancedSettingComps.length) {
const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{fontSize: 14}} className={iconName}></i> {_('Show Advanced Settings')}</button>;
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
}
return (
<div key={key} style={sectionStyle}>
{noteComp}
<div>{settingComps}</div>
{advancedSettingsButton}
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
</div>
);
}
@@ -413,6 +490,24 @@ class ConfigScreenComponent extends React.Component {
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_BUTTON) {
const theme = themeStyle(this.props.theme);
const buttonStyle = Object.assign({}, theme.buttonStyle, {
display: 'inline-block',
marginRight: 10,
});
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<button style={buttonStyle} onClick={md.onClick}>
{_('Edit')}
</button>
{descriptionComp}
</div>
);
} else {
console.warn(`Type not implemented: ${key}`);
}

View File

@@ -0,0 +1,45 @@
const React = require('react');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
function DialogButtonRow(props) {
const theme = themeStyle(props.theme);
const okButton_click = () => {
if (props.onClick) props.onClick({ buttonName: 'ok' });
};
const cancelButton_click = () => {
if (props.onClick) props.onClick({ buttonName: 'cancel' });
};
const onKeyDown = (event) => {
if (event.keyCode === 13) {
okButton_click();
} else if (event.keyCode === 27) {
cancelButton_click();
}
};
const buttonComps = [];
if (props.okButtonShow !== false) {
buttonComps.push(
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{_('OK')}
</button>
);
}
if (props.cancelButtonShow !== false) {
buttonComps.push(
<button key="cancel" style={Object.assign({}, theme.buttonStyle, { marginLeft: 10 })} onClick={cancelButton_click}>
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
</button>
);
}
return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
}
module.exports = DialogButtonRow;

View File

@@ -13,7 +13,7 @@ class DropboxLoginScreenComponent extends React.Component {
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg));
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.shared_.refreshUrl();
}

View File

@@ -31,11 +31,11 @@ class EncryptionConfigScreenComponent extends React.Component {
return shared.refreshStats(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.initState(this.props);
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
this.initState(nextProps);
}

View File

@@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
};
}
async componentWillReceiveProps(nextProps) {
async UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.windowCommand) {
this.doCommand(nextProps.windowCommand);
}

View File

@@ -8,7 +8,7 @@ const { filename, basename } = require('lib/path-utils.js');
const { importEnex } = require('lib/import-enex');
class ImportScreenComponent extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({
doImport: true,
filePath: this.props.filePath,
@@ -16,7 +16,7 @@ class ImportScreenComponent extends React.Component {
});
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.filePath) {
this.setState(
{

View File

@@ -23,7 +23,7 @@ class ItemList extends React.Component {
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
const visibleItemCount = this.visibleItemCount(props);
let bottomItemIndex = topItemIndex + visibleItemCount;
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
this.setState({
@@ -32,11 +32,11 @@ class ItemList extends React.Component {
});
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.updateStateItemIndexes();
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
this.updateStateItemIndexes(newProps);
}
@@ -50,7 +50,7 @@ class ItemList extends React.Component {
}
makeItemIndexVisible(itemIndex) {
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex + 1);
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
const bottom = Math.max(0, this.state.bottomItemIndex);
if (itemIndex >= top && itemIndex <= bottom) return;

View File

@@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
const ShareNoteDialog = require('./ShareNoteDialog.js').default;
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const Tag = require('lib/models/Tag.js');
@@ -24,6 +25,7 @@ class MainScreenComponent extends React.Component {
super();
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
this.noteList_onDrag = this.noteList_onDrag.bind(this);
}
@@ -40,7 +42,11 @@ class MainScreenComponent extends React.Component {
this.setState({ notePropertiesDialogOptions: {} });
}
componentWillMount() {
shareNoteDialog_close() {
this.setState({ shareNoteDialogOptions: {} });
}
UNSAFE_componentWillMount() {
this.setState({
promptOptions: null,
modalLayer: {
@@ -48,10 +54,11 @@ class MainScreenComponent extends React.Component {
message: '',
},
notePropertiesDialogOptions: {},
shareNoteDialogOptions: {},
});
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.windowCommand) {
this.doCommand(newProps.windowCommand);
}
@@ -247,6 +254,13 @@ class MainScreenComponent extends React.Component {
onRevisionLinkClick: command.onRevisionLinkClick,
},
});
} else if (command.name === 'commandShareNoteDialog') {
this.setState({
shareNoteDialogOptions: {
noteIds: command.noteIds,
visible: true,
},
});
} else if (command.name === 'toggleVisiblePanes') {
this.toggleVisiblePanes();
} else if (command.name === 'toggleSidebar') {
@@ -362,14 +376,16 @@ class MainScreenComponent extends React.Component {
backgroundColor: theme.warningBackgroundColor,
};
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
this.styles_.verticalResizer = {
width: 5,
height: height,
// HACK: For unknown reasons, the resizers are just a little bit taller than the other elements,
// making the whole window scroll vertically. So we remove 10 extra pixels here.
height: rowHeight - 10,
display: 'inline-block',
};
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
this.styles_.sideBar = {
width: sidebarWidth - this.styles_.verticalResizer.width,
height: rowHeight,
@@ -573,6 +589,7 @@ class MainScreenComponent extends React.Component {
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
const keyboardMode = Setting.value('editor.keyboardMode');
return (
@@ -580,6 +597,7 @@ class MainScreenComponent extends React.Component {
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
@@ -589,7 +607,7 @@ class MainScreenComponent extends React.Component {
<VerticalResizer style={styles.verticalResizer} onDrag={this.sidebar_onDrag} />
<NoteList style={styles.noteList} />
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag} />
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} noteDevToolsVisible={this.props.noteDevToolsVisible} />
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} />
{pluginDialog}
</div>
@@ -613,7 +631,6 @@ const mapStateToProps = state => {
noteListWidth: state.settings['style.noteList.width'],
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
plugins: state.plugins,
noteDevToolsVisible: state.noteDevToolsVisible,
templates: state.templates,
};
};

View File

@@ -4,7 +4,7 @@ const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
class NavigatorComponent extends Component {
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.route) {
const screenInfo = this.props.screens[newProps.route.routeName];
let windowTitle = ['Joplin'];

View File

@@ -87,6 +87,7 @@ class NoteListComponent extends React.Component {
const menu = NoteListUtils.makeContextMenu(noteIds, {
notes: this.props.notes,
dispatch: this.props.dispatch,
watchedNoteFiles: this.props.watchedNoteFiles,
});
menu.popup(bridge().window());

View File

@@ -2,6 +2,7 @@ const React = require('react');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
const { time } = require('lib/time-utils.js');
const DialogButtonRow = require('./DialogButtonRow.min');
const Datetime = require('react-datetime');
const Note = require('lib/models/Note');
const formatcoords = require('formatcoords');
@@ -11,10 +12,8 @@ class NotePropertiesDialog extends React.Component {
constructor() {
super();
this.okButton_click = this.okButton_click.bind(this);
this.cancelButton_click = this.cancelButton_click.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.revisionsLink_click = this.revisionsLink_click.bind(this);
this.buttonRow_click = this.buttonRow_click.bind(this);
this.okButton = React.createRef();
this.state = {
@@ -27,6 +26,7 @@ class NotePropertiesDialog extends React.Component {
id: _('ID'),
user_created_time: _('Created'),
user_updated_time: _('Updated'),
todo_completed: _('Completed'),
location: _('Location'),
source_url: _('URL'),
revisionsLink: _('Note History'),
@@ -72,6 +72,11 @@ class NotePropertiesDialog extends React.Component {
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
formNote.user_created_time = time.formatMsToLocal(note.user_created_time);
if (note.todo_completed) {
formNote.todo_completed = time.formatMsToLocal(note.todo_completed);
}
formNote.source_url = note.source_url;
formNote.location = '';
@@ -90,6 +95,11 @@ class NotePropertiesDialog extends React.Component {
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
if (formNote.todo_completed) {
note.todo_completed = time.formatMsToLocal(formNote.todo_completed);
}
note.source_url = formNote.source_url;
return note;
@@ -107,6 +117,9 @@ class NotePropertiesDialog extends React.Component {
this.styles_.controlBox = {
marginBottom: '1em',
color: 'black', // This will apply for the calendar
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
};
this.styles_.button = {
@@ -123,8 +136,11 @@ class NotePropertiesDialog extends React.Component {
color: theme.color,
textDecoration: 'none',
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
padding: '.14em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '0.5em',
};
this.styles_.input = {
@@ -153,12 +169,8 @@ class NotePropertiesDialog extends React.Component {
}
}
okButton_click() {
this.closeDialog(true);
}
cancelButton_click() {
this.closeDialog(false);
buttonRow_click(event) {
this.closeDialog(event.buttonName === 'ok');
}
revisionsLink_click() {
@@ -166,14 +178,6 @@ class NotePropertiesDialog extends React.Component {
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
}
onKeyDown(event) {
if (event.keyCode === 13) {
this.closeDialog(true);
} else if (event.keyCode === 27) {
this.closeDialog(false);
}
}
editPropertyButtonClick(key, initialValue) {
this.setState({
editedKey: key,
@@ -218,15 +222,12 @@ class NotePropertiesDialog extends React.Component {
async cancelProperty() {
return new Promise((resolve) => {
this.okButton.current.focus();
this.setState(
{
editedKey: null,
editedValue: null,
},
() => {
resolve();
}
);
this.setState({
editedKey: null,
editedValue: null,
}, () => {
resolve();
});
});
}
@@ -328,7 +329,7 @@ class NotePropertiesDialog extends React.Component {
if (editCompHandler) {
editComp = (
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}>
<i className={`fa ${editCompIcon}`} aria-hidden="true" style={{ marginLeft: '.5em' }}></i>
<i className={`fa ${editCompIcon}`} aria-hidden="true"></i>
</a>
);
}
@@ -354,7 +355,7 @@ class NotePropertiesDialog extends React.Component {
return dms.format('DDMMss', { decimalPlaces: 0 });
}
if (['user_updated_time', 'user_created_time'].indexOf(key) >= 0) {
if (['user_updated_time', 'user_created_time', 'todo_completed'].indexOf(key) >= 0) {
return time.formatMsToLocal(note[key]);
}
@@ -363,21 +364,8 @@ class NotePropertiesDialog extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const styles = this.styles(this.props.theme);
const formNote = this.state.formNote;
const buttonComps = [];
buttonComps.push(
<button key="ok" style={styles.button} onClick={this.okButton_click} ref={this.okButton} onKeyDown={this.onKeyDown}>
{_('Apply')}
</button>
);
buttonComps.push(
<button key="cancel" style={styles.button} onClick={this.cancelButton_click}>
{_('Cancel')}
</button>
);
const noteComps = [];
if (formNote) {
@@ -393,7 +381,7 @@ class NotePropertiesDialog extends React.Component {
<div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Note properties')}</div>
<div>{noteComps}</div>
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
</div>
</div>
);

View File

@@ -6,16 +6,16 @@ const NoteTextViewer = require('./NoteTextViewer.min');
const HelpButton = require('./HelpButton.min');
const BaseModel = require('lib/BaseModel');
const Revision = require('lib/models/Revision');
const Note = require('lib/models/Note');
const urlUtils = require('lib/urlUtils');
const Setting = require('lib/models/Setting');
const RevisionService = require('lib/services/RevisionService');
const shared = require('lib/components/shared/note-screen-shared.js');
const MarkupToHtml = require('lib/renderers/MarkupToHtml');
const { MarkupToHtml, assetsToHeaders } = require('joplin-renderer');
const { time } = require('lib/time-utils.js');
const ReactTooltip = require('react-tooltip');
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
const { bridge } = require('electron').remote.require('./bridge');
const markupLanguageUtils = require('lib/markupLanguageUtils');
class NoteRevisionViewerComponent extends React.PureComponent {
constructor() {
@@ -101,7 +101,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
async reloadNote() {
let noteBody = '';
let markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
if (!this.state.revisions.length || !this.state.currentRevId) {
noteBody = _('This note has no history');
this.setState({ note: null });
@@ -116,18 +116,22 @@ class NoteRevisionViewerComponent extends React.PureComponent {
const theme = themeStyle(this.props.theme);
const markupToHtml = new MarkupToHtml({
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
});
const result = markupToHtml.render(markupLanguage, noteBody, theme, {
const result = await markupToHtml.render(markupLanguage, noteBody, theme, {
codeTheme: theme.codeThemeCss,
userCss: this.props.customCss ? this.props.customCss : '',
resources: await shared.attachedResources(noteBody),
postMessageSyntax: 'ipcProxySendToHost',
});
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles });
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, {
cssFiles: result.cssFiles,
pluginAssets: result.pluginAssets,
pluginAssetsHeadersHtml: assetsToHeaders(result.pluginAssets),
});
}
async webview_ipcMessage(event) {
@@ -196,7 +200,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
</div>
);
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1 }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
return (
<div style={style.root}>

View File

@@ -4,9 +4,9 @@ const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
const Resource = require('lib/models/Resource.js');
const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js');
const { time } = require('lib/time-utils.js');
const Setting = require('lib/models/Setting.js');
const InteropServiceHelper = require('../InteropServiceHelper.js');
const { IconButton } = require('./IconButton.min.js');
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
const Toolbar = require('./Toolbar.min.js');
@@ -14,7 +14,7 @@ const TagList = require('./TagList.min.js');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
const MarkupToHtml = require('lib/renderers/MarkupToHtml');
const { MarkupToHtml, assetsToHeaders } = require('joplin-renderer');
const shared = require('lib/components/shared/note-screen-shared.js');
const { bridge } = require('electron').remote.require('./bridge');
const { themeStyle } = require('../theme.js');
@@ -40,6 +40,7 @@ const SearchEngine = require('lib/services/SearchEngine');
const NoteTextViewer = require('./NoteTextViewer.min');
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
const TemplateUtils = require('lib/TemplateUtils');
const markupLanguageUtils = require('lib/markupLanguageUtils');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@@ -79,7 +80,7 @@ class CustomMdMode extends ace.acequire('ace/mode/markdown').Mode {
}
}
const NOTE_TAG_BAR_FEATURE_ENABLED = false;
const NOTE_TAG_BAR_FEATURE_ENABLED = true;
class NoteTextComponent extends React.Component {
constructor() {
@@ -110,6 +111,7 @@ class NoteTextComponent extends React.Component {
newAndNoTitleChangeNoteId: null,
bodyHtml: '',
lastRenderCssFiles: [],
lastRenderPluginAssets: [],
lastKeys: [],
showLocalSearch: false,
localSearch: Object.assign({}, this.localSearchDefaultState),
@@ -400,13 +402,15 @@ class NoteTextComponent extends React.Component {
markupToHtml() {
if (this.markupToHtml_) return this.markupToHtml_;
this.markupToHtml_ = new MarkupToHtml({
this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
});
return this.markupToHtml_;
}
async componentWillMount() {
async UNSAFE_componentWillMount() {
let note = null;
let noteTags = [];
if (this.props.newNote) {
@@ -454,19 +458,6 @@ class NoteTextComponent extends React.Component {
}
componentDidUpdate() {
// if (Setting.value('env') === 'dev' && this.webviewRef()) {
// this.webviewRef().openDevTools();
// return;
// }
if (this.webviewRef() && this.props.noteDevToolsVisible !== this.webviewRef().isDevToolsOpened()) {
if (this.props.noteDevToolsVisible) {
this.webviewRef().openDevTools();
} else {
this.webviewRef().closeDevTools();
}
}
const currentNoteId = this.state.note ? this.state.note.id : null;
if (this.lastComponentUpdateNoteId_ !== currentNoteId && this.editor_) {
this.editor_.editor.getSession().setMode(new CustomMdMode());
@@ -547,7 +538,6 @@ class NoteTextComponent extends React.Component {
let note = null;
let loadingNewNote = true;
let parentFolder = null;
let noteTags = [];
let scrollPercent = 0;
if (props.newNote) {
@@ -562,7 +552,6 @@ class NoteTextComponent extends React.Component {
if (!scrollPercent) scrollPercent = 0;
loadingNewNote = stateNoteId !== noteId;
noteTags = await Tag.tagsByNoteId(noteId);
this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return defer(); // Race condition - current note was changed while this one was loading
@@ -641,7 +630,6 @@ class NoteTextComponent extends React.Component {
webviewReady: webviewReady,
folder: parentFolder,
lastKeys: [],
noteTags: noteTags,
showRevisions: false,
};
@@ -662,22 +650,6 @@ class NoteTextComponent extends React.Component {
this.setState(newState);
// https://github.com/laurent22/joplin/pull/893#discussion_r228025210
// @Abijeet: Had to add this check. If not, was going into an infinite loop where state was getting updated repeatedly.
// Since I'm updating the state, the componentWillReceiveProps was getting triggered again, where nextProps.newNote was still true, causing reloadNote to trigger again and again.
// Notes from Laurent: The selected note tags are part of the global Redux state because they need to be updated whenever tags are changed or deleted
// anywhere in the app. Thus it's not possible simple to load the tags here (as we won't have a way to know if they're updated afterwards).
// Perhaps a better way would be to move that code in the middleware, check for TAGS_DELETE, TAGS_UPDATE, etc. actions and update the
// selected note tags accordingly.
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
if (!this.props.newNote) {
this.props.dispatch({
type: 'SET_NOTE_TAGS',
items: noteTags,
});
}
}
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
await this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : '');
@@ -685,7 +657,7 @@ class NoteTextComponent extends React.Component {
defer();
}
async componentWillReceiveProps(nextProps) {
async UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.newNote !== nextProps.newNote && nextProps.newNote) {
await this.scheduleReloadNote(nextProps);
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
@@ -717,18 +689,38 @@ class NoteTextComponent extends React.Component {
if (newTags.length !== oldTags.length) return true;
for (let i = 0; i < newTags.length; ++i) {
let found = false;
let currNewTag = newTags[i];
for (let j = 0; j < oldTags.length; ++j) {
let currOldTag = oldTags[j];
if (currOldTag.id === currNewTag.id && currOldTag.updated_time !== currNewTag.updated_time) {
return true;
if (currOldTag.id === currNewTag.id) {
found = true;
if (currOldTag.updated_time !== currNewTag.updated_time) {
return true;
}
break;
}
}
if (!found) {
return true;
}
}
return false;
}
canDisplayTagBar() {
if (!NOTE_TAG_BAR_FEATURE_ENABLED) {
return false;
}
if (!this.state.noteTags || this.state.noteTags.length === 0) {
return false;
}
return true;
}
async noteRevisionViewer_onBack() {
// When coming back from the revision viewer, the webview has been
// unmounted so will need to reload. We set webviewReady to false
@@ -855,7 +847,11 @@ class NoteTextComponent extends React.Component {
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const localState = await Resource.localState(item);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!item.encryption_blob_encrypted) {
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet.'));
if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) {
bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`);
} else {
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet'));
}
return;
}
const filePath = Resource.fullPath(item);
@@ -942,6 +938,8 @@ class NoteTextComponent extends React.Component {
}
webview_domReady() {
console.info('webview_domReady', this.webviewRef_.current);
if (!this.webviewRef_.current) return;
this.setState({
@@ -1054,10 +1052,10 @@ class NoteTextComponent extends React.Component {
if (bodyToRender === null) {
bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
markupLanguage = this.state.note ? this.state.note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
markupLanguage = this.state.note ? this.state.note.markup_language : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
}
if (!markupLanguage) markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
if (!markupLanguage) markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
const resources = await shared.attachedResources(bodyToRender);
@@ -1078,11 +1076,11 @@ class NoteTextComponent extends React.Component {
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
}
const result = this.markupToHtml().render(markupLanguage, bodyToRender, theme, mdOptions);
const result = await this.markupToHtml().render(markupLanguage, bodyToRender, theme, mdOptions);
this.setState({
bodyHtml: result.html,
lastRenderCssFiles: result.cssFiles,
lastRenderPluginAssets: result.pluginAssets,
});
}
@@ -1113,9 +1111,11 @@ class NoteTextComponent extends React.Component {
if (!command) return;
let fn = null;
let args = null;
if (command.name === 'exportPdf') {
fn = this.commandSavePdf;
args = {noteId: command.noteId};
} else if (command.name === 'print') {
fn = this.commandPrint;
}
@@ -1131,6 +1131,8 @@ class NoteTextComponent extends React.Component {
fn = this.commandDateTime;
} else if (command.name === 'commandStartExternalEditing') {
fn = this.commandStartExternalEditing;
} else if (command.name === 'commandStopExternalEditing') {
fn = this.commandStopExternalEditing;
} else if (command.name === 'showLocalSearch') {
fn = this.commandShowLocalSearch;
} else if (command.name === 'textCode') {
@@ -1165,7 +1167,7 @@ class NoteTextComponent extends React.Component {
requestAnimationFrame(() => {
fn = fn.bind(this);
fn();
fn(args);
});
}
@@ -1224,11 +1226,6 @@ class NoteTextComponent extends React.Component {
});
}
// helper function to style the title for printing
title_(title) {
return `<div style="font-size: 2em; font-weight: bold; border-bottom: 1px solid rgb(230,230,230); padding-bottom: .3em;">${title}</div><br>`;
}
async printTo_(target, options) {
if (this.props.selectedNoteIds.length !== 1 || !this.webviewRef_.current) {
throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
@@ -1240,53 +1237,71 @@ class NoteTextComponent extends React.Component {
}
this.isPrinting_ = true;
const previousBody = this.state.note.body;
const tempBody = `${this.title_(this.state.note.title)}\n\n${previousBody}`;
const previousTheme = Setting.value('theme');
Setting.setValue('theme', Setting.THEME_LIGHT);
this.lastSetHtml_ = '';
await this.updateHtml(this.state.note.markup_language, tempBody, { useCustomCss: true });
this.forceUpdate();
// const previousBody = this.state.note.body;
// const tempBody = `${this.state.note.title}\n\n${previousBody}`;
const restoreSettings = async () => {
Setting.setValue('theme', previousTheme);
this.lastSetHtml_ = '';
await this.updateHtml(this.state.note.markup_language, previousBody);
this.forceUpdate();
};
// const previousTheme = Setting.value('theme');
// Setting.setValue('theme', Setting.THEME_LIGHT);
// this.lastSetHtml_ = '';
// await this.updateHtml(this.state.note.markup_language, tempBody, { useCustomCss: true });
// this.forceUpdate();
setTimeout(() => {
// const restoreSettings = async () => {
// Setting.setValue('theme', previousTheme);
// this.lastSetHtml_ = '';
// await this.updateHtml(this.state.note.markup_language, previousBody);
// this.forceUpdate();
// };
// Need to save because the interop service reloads the note from the database
await this.saveIfNeeded();
setTimeout(async () => {
if (target === 'pdf') {
this.webviewRef_.current.wrappedInstance.printToPDF({ printBackground: true, pageSize: Setting.value('export.pdfPageSize'), landscape: Setting.value('export.pdfPageOrientation') === 'landscape' }, (error, data) => {
restoreSettings();
if (error) {
bridge().showErrorMessageBox(error.message);
} else {
shim.fsDriver().writeFile(options.path, data, 'buffer');
}
});
try {
const pdfData = await InteropServiceHelper.exportNoteToPdf(options.noteId, {
printBackground: true,
pageSize: Setting.value('export.pdfPageSize'),
landscape: Setting.value('export.pdfPageOrientation') === 'landscape',
customCss: this.props.customCss,
});
await shim.fsDriver().writeFile(options.path, pdfData, 'buffer');
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
} else if (target === 'printer') {
this.webviewRef_.current.wrappedInstance.print({ printBackground: true });
restoreSettings();
try {
await InteropServiceHelper.printNote(options.noteId, {
printBackground: true,
customCss: this.props.customCss,
});
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
// restoreSettings();
}
this.isPrinting_ = false;
}, 100);
}
async commandSavePdf() {
async commandSavePdf(args) {
try {
if (!this.state.note) throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
if (!this.state.note && !args.noteId) throw new Error(_('Only one note can be exported to PDF at a time.'));
const note = (!args.noteId ? this.state.note : await Note.load(args.noteId));
const path = bridge().showSaveDialog({
filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
defaultPath: safeFilename(this.state.note.title),
defaultPath: safeFilename(note.title),
});
if (!path) return;
await this.printTo_('pdf', { path: path });
await this.printTo_('pdf', { path: path, noteId: note.id });
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
@@ -1294,25 +1309,23 @@ class NoteTextComponent extends React.Component {
async commandPrint() {
try {
await this.printTo_('printer');
if (!this.state.note) throw new Error(_('Only one note can be printed at a time.'));
await this.printTo_('printer', { noteId: this.state.note.id });
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
}
async commandStartExternalEditing() {
try {
await this.saveIfNeeded(true, {
autoTitle: false,
});
await ExternalEditWatcher.instance().openAndWatch(this.state.note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}
await this.saveIfNeeded(true, {
autoTitle: false,
});
NoteListUtils.startExternalEditing(this.state.note.id);
}
async commandStopExternalEditing() {
ExternalEditWatcher.instance().stopWatching(this.state.note.id);
NoteListUtils.stopExternalEditing(this.state.note.id);
}
async commandSetTags() {
@@ -1627,7 +1640,7 @@ class NoteTextComponent extends React.Component {
});
}
if (note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN && editorIsVisible) {
if (note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN && editorIsVisible) {
toolbarItems.push({
tooltip: _('Bold'),
iconName: 'fa-bold',
@@ -1818,6 +1831,7 @@ class NoteTextComponent extends React.Component {
const menu = NoteListUtils.makeContextMenu(this.props.selectedNoteIds, {
notes: this.props.notes,
dispatch: this.props.dispatch,
watchedNoteFiles: this.props.watchedNoteFiles,
});
const buttonStyle = Object.assign({}, theme.buttonStyle, {
@@ -1855,7 +1869,7 @@ class NoteTextComponent extends React.Component {
const style = this.props.style;
const note = this.state.note;
const body = note && note.body ? note.body : '';
const markupLanguage = note ? note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
const markupLanguage = note ? note.markup_language : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
const theme = themeStyle(this.props.theme);
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
const isTodo = note && !!note.is_todo;
@@ -1933,10 +1947,10 @@ class NoteTextComponent extends React.Component {
const searchBarHeight = this.state.showLocalSearch ? 35 : 0;
let bottomRowHeight = 0;
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
if (this.canDisplayTagBar()) {
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - tagStyle.height - tagStyle.marginBottom;
} else {
toolbarStyle.marginBottom = 10;
toolbarStyle.marginBottom = tagStyle.marginBottom,
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
}
@@ -1996,7 +2010,8 @@ class NoteTextComponent extends React.Component {
const htmlHasChanged = this.lastSetHtml_ !== html;
if (htmlHasChanged) {
let options = {
cssFiles: this.state.lastRenderCssFiles,
pluginAssets: this.state.lastRenderPluginAssets,
pluginAssetsHeadersHtml: assetsToHeaders(this.state.lastRenderPluginAssets),
downloadResources: Setting.value('sync.resourceDownloadMode'),
};
this.webviewRef_.current.wrappedInstance.send('setHtml', html, options);
@@ -2054,7 +2069,7 @@ class NoteTextComponent extends React.Component {
/>
);
const tagList = !NOTE_TAG_BAR_FEATURE_ENABLED ? null : <TagList style={tagStyle} items={this.state.noteTags} />;
const tagList = this.canDisplayTagBar() ? <TagList style={tagStyle} items={this.state.noteTags} /> : null;
const titleBarMenuButton = (
<IconButton
@@ -2112,6 +2127,8 @@ class NoteTextComponent extends React.Component {
onSelectionChange={this.aceEditor_selectionChange}
onFocus={this.aceEditor_focus}
readOnly={visiblePanes.indexOf('editor') < 0}
// Enable/Disable the autoclosing braces
setOptions={{ behavioursEnabled: Setting.value('editor.autoMatchingBraces') }}
// Disable warning: "Automatically scrolling cursor into view after
// selection change this will be disabled in the next version set
// editor.$blockScrolling = Infinity to disable this message"

View File

@@ -13,6 +13,8 @@ class NoteTextViewerComponent extends React.Component {
this.webview_domReady = this.webview_domReady.bind(this);
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
this.webview_load = this.webview_load.bind(this);
this.webview_message = this.webview_message.bind(this);
}
webview_domReady(event) {
@@ -24,6 +26,22 @@ class NoteTextViewerComponent extends React.Component {
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
}
webview_load() {
this.webview_domReady();
}
webview_message(event) {
if (!event.data || event.data.target !== 'main') return;
const callName = event.data.name;
const args = event.data.args;
if (this.props.onIpcMessage) this.props.onIpcMessage({
channel: callName,
args: args,
});
}
domReady() {
return this.domReady_;
}
@@ -35,6 +53,7 @@ class NoteTextViewerComponent extends React.Component {
this.webviewListeners_ = {
'dom-ready': this.webview_domReady.bind(this),
'ipc-message': this.webview_ipcMessage.bind(this),
'load': this.webview_load.bind(this),
};
}
@@ -44,19 +63,7 @@ class NoteTextViewerComponent extends React.Component {
wv.addEventListener(n, fn);
}
let isAlreadyReady = false;
try {
isAlreadyReady = !this.webviewRef_.current.isLoading();
} catch (error) {
// Ignore - it means the view has not started loading, and the DOM ready event has not been emitted yet
// Error is "The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called."
}
// Edge-case - the webview was already ready by the time initWebview was
// called - so manually call the domReady event to notify caller.
if (isAlreadyReady) {
this.webview_domReady({});
}
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
}
destroyWebview() {
@@ -69,6 +76,8 @@ class NoteTextViewerComponent extends React.Component {
wv.removeEventListener(n, fn);
}
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
this.initialized_ = false;
this.domReady_ = false;
}
@@ -96,16 +105,32 @@ class NoteTextViewerComponent extends React.Component {
// Wrap WebView functions
// ----------------------------------------------------------------
send(channel, arg0 = null, arg1 = null, arg2 = null, arg3 = null) {
return this.webviewRef_.current.send(channel, arg0, arg1, arg2, arg3);
send(channel, arg0 = null, arg1 = null) {
const win = this.webviewRef_.current.contentWindow;
if (channel === 'setHtml') {
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
}
if (channel === 'scrollToHash') {
win.postMessage({ target: 'webview', name: 'scrollToHash', data: { hash: arg0 } }, '*');
}
if (channel === 'setPercentScroll') {
win.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: arg0 } }, '*');
}
if (channel === 'setMarkers') {
win.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: arg0, options: arg1 } }, '*');
}
}
printToPDF(options, callback) {
printToPDF() { // options, callback) {
// In Electron 4x, printToPDF is broken so need to use this hack:
// https://github.com/electron/electron/issues/16171#issuecomment-451090245
// return this.webviewRef_.current.printToPDF(options, callback);
return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
// return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
}
print() {
@@ -119,15 +144,15 @@ class NoteTextViewerComponent extends React.Component {
}
openDevTools() {
return this.webviewRef_.current.openDevTools();
// return this.webviewRef_.current.openDevTools();
}
closeDevTools() {
return this.webviewRef_.current.closeDevTools();
// return this.webviewRef_.current.closeDevTools();
}
isDevToolsOpened() {
return this.webviewRef_.current.isDevToolsOpened();
// return this.webviewRef_.current.isDevToolsOpened();
}
// ----------------------------------------------------------------
@@ -135,7 +160,8 @@ class NoteTextViewerComponent extends React.Component {
// ----------------------------------------------------------------
render() {
return <webview ref={this.webviewRef_} style={this.props.viewerStyle} preload="gui/note-viewer/preload.js" src="gui/note-viewer/index.html" webpreferences="contextIsolation" />;
const viewerStyle = Object.assign({}, this.props.viewerStyle, { borderTop: 'none', borderRight: 'none', borderBottom: 'none' });
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
}
}

View File

@@ -1,108 +1,76 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const Setting = require('lib/models/Setting');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
class OneDriveLoginScreenComponent extends React.Component {
constructor() {
super();
this.webview_ = null;
this.authCode_ = null;
this.state = {
authLog: [],
};
}
refresh_click() {
if (!this.webview_) return;
this.webview_.src = this.startUrl();
}
async componentDidMount() {
const log = (s) => {
this.setState(state => {
const authLog = state.authLog.slice();
authLog.push({ key: (Date.now() + Math.random()).toString(), text: s });
return { authLog: authLog };
});
};
componentWillMount() {
this.setState({
webviewUrl: this.startUrl(),
webviewReady: false,
const syncTargetId = Setting.value('sync.target');
const syncTarget = reg.syncTarget(syncTargetId);
const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await oneDriveApiUtils.oauthDance({
log: (s) => log(s),
});
}
componentDidMount() {
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
}
Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null);
syncTarget.api().setAuth(auth);
componentWillUnmount() {
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
}
webview_domReady() {
this.setState({ webviewReady: true });
this.webview_.addEventListener('did-navigate', async event => {
const url = event.url;
if (this.authCode_) return;
const urlParse = require('url').parse;
const parsedUrl = urlParse(url.trim(), true);
if (!('code' in parsedUrl.query)) return;
this.authCode_ = parsedUrl.query.code;
try {
await reg
.syncTarget()
.api()
.execTokenRequest(this.authCode_, this.redirectUrl(), true);
this.props.dispatch({ type: 'NAV_BACK' });
reg.scheduleSync(0);
} catch (error) {
bridge().showErrorMessageBox(`Could not login to OneDrive. Please try again.\n\n${error.message}\n\n${url.match(/.{1,64}/g).join('\n')}`);
}
this.authCode_ = null;
});
if (!auth) {
log(_('Authentication was not completed (did not receive an authentication token).'));
} else {
reg.scheduleSync(0);
}
}
startUrl() {
return reg
.syncTarget()
.api()
.authCodeUrl(this.redirectUrl());
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
}
redirectUrl() {
return reg
.syncTarget()
.api()
.nativeClientRedirectUrl();
return reg.syncTarget().api().nativeClientRedirectUrl();
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
const webviewStyle = {
width: this.props.style.width,
height: this.props.style.height - theme.headerHeight,
overflow: 'hidden',
color: theme.color,
backgroundColor: theme.backgroundColor,
};
const headerButtons = [
{
title: _('Refresh'),
onClick: () => this.refresh_click(),
iconName: 'fa-refresh',
},
];
const logComps = [];
for (const l of this.state.authLog) {
if (l.text.indexOf('http:') === 0) {
logComps.push(<a key={l.key} style={theme.urlStyle} href="#" onClick={() => { bridge().openExternal(l.text); }}>{l.text}</a>);
} else {
logComps.push(<p key={l.key} style={theme.textStyle}>{l.text}</p>);
}
}
return (
<div>
<Header style={headerStyle} buttons={headerButtons} />
<webview src={this.startUrl()} style={webviewStyle} nodeintegration="1" ref={elem => (this.webview_ = elem)} />
<Header style={headerStyle}/>
<div style={{padding: 10}}>
{logComps}
</div>
</div>
);
}

View File

@@ -14,7 +14,7 @@ class PromptDialog extends React.Component {
this.answerInput_ = React.createRef();
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({
visible: false,
answer: this.props.defaultValue ? this.props.defaultValue : '',
@@ -22,7 +22,7 @@ class PromptDialog extends React.Component {
this.focusInput_ = true;
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if ('visible' in newProps && newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;

View File

@@ -0,0 +1,219 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import JoplinServerApi from '../lib/JoplinServerApi';
const { _, _n } = require('lib/locale.js');
const { themeStyle, buildStyle } = require('../theme.js');
const DialogButtonRow = require('./DialogButtonRow.min');
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting');
const BaseItem = require('lib/models/BaseItem');
const { reg } = require('lib/registry.js');
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
theme: number,
noteIds: Array<string>,
onClose: Function,
}
interface SharesMap {
[key: string]: any;
}
function styles_(props:ShareNoteDialogProps) {
return buildStyle('ShareNoteDialog', props.theme, (theme:any) => {
return {
noteList: {
marginBottom: 10,
},
note: {
flex: 1,
flexDirection: 'row',
display: 'flex',
alignItems: 'center',
border: '1px solid',
borderColor: theme.dividerColor,
padding: '0.5em',
marginBottom: 5,
},
noteTitle: {
...theme.textStyle,
flex: 1,
display: 'flex',
color: theme.color,
},
noteRemoveButton: {
background: 'none',
border: 'none',
},
noteRemoveButtonIcon: {
color: theme.color,
fontSize: '1.4em',
},
copyShareLinkButton: {
...theme.buttonStyle,
marginBottom: 10,
},
};
});
}
export default function ShareNoteDialog(props:ShareNoteDialogProps) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<any[]>([]);
const [sharesState, setSharesState] = useState<string>('unknown');
const [shares, setShares] = useState<SharesMap>({});
const noteCount = notes.length;
const theme = themeStyle(props.theme);
const styles = styles_(props);
useEffect(() => {
async function fetchNotes() {
const result = [];
for (let noteId of props.noteIds) {
result.push(await Note.load(noteId));
}
setNotes(result);
}
fetchNotes();
}, [props.noteIds]);
const appApi = async () => {
return reg.syncTargetNextcloud().appApi();
};
const buttonRow_click = () => {
props.onClose();
};
const copyLinksToClipboard = (shares:SharesMap) => {
const links = [];
for (const n in shares) links.push(shares[n]._url);
clipboard.writeText(links.join('\n'));
};
const synchronize = async () => {
const synchronizer = await reg.syncTarget().synchronizer();
await synchronizer.waitForSyncToFinish();
await reg.scheduleSync(0);
};
const shareLinkButton_click = async () => {
let hasSynced = false;
let tryToSync = false;
while (true) {
try {
if (tryToSync) {
setSharesState('synchronizing');
await synchronize();
tryToSync = false;
hasSynced = true;
}
setSharesState('creating');
const api = await appApi();
const syncTargetId = api.syncTargetId(Setting.toPlainObject());
const newShares = Object.assign({}, shares);
let sharedStatusChanged = false;
for (const note of notes) {
const result = await api.exec('POST', 'shares', {
syncTargetId: syncTargetId,
noteId: note.id,
});
newShares[note.id] = result;
const changed = await BaseItem.updateShareStatus(note, true);
if (changed) sharedStatusChanged = true;
}
setShares(newShares);
if (sharedStatusChanged) {
setSharesState('synchronizing');
await synchronize();
setSharesState('creating');
}
copyLinksToClipboard(newShares);
setSharesState('created');
} catch (error) {
if (error.code === 404 && !hasSynced) {
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
tryToSync = true;
continue;
}
reg.logger().error('ShareNoteDialog: Cannot share note:', error);
setSharesState('idle');
alert(JoplinServerApi.connectionErrorMessage(error));
}
break;
}
};
const removeNoteButton_click = (event:any) => {
const newNotes = [];
for (let i = 0; i < notes.length; i++) {
const n = notes[i];
if (n.id === event.noteId) continue;
newNotes.push(n);
}
setNotes(newNotes);
};
const renderNote = (note:any) => {
const removeButton = notes.length <= 1 ? null : (
<button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
<i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
</button>
);
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{removeButton}
</div>
);
};
const renderNoteList = (notes:any) => {
const noteComps = [];
for (let noteId of Object.keys(notes)) {
noteComps.push(renderNote(notes[noteId]));
}
return <div style={styles.noteList}>{noteComps}</div>;
};
const statusMessage = (sharesState:string):string => {
if (sharesState === 'synchronizing') return _('Synchronising...');
if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
return '';
};
const encryptionWarningMessage = !Setting.value('encryption.enabled') ? null : <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}</div>;
const rootStyle = Object.assign({}, theme.dialogBox);
rootStyle.width = '50%';
return (
<div style={theme.dialogModalLayer}>
<div style={rootStyle}>
<div style={theme.dialogTitle}>{_('Share Notes')}</div>
{renderNoteList(notes)}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{encryptionWarningMessage}
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>
);
}

View File

@@ -221,6 +221,10 @@ class SideBarComponent extends React.Component {
if (item) {
const anchorRef = this.anchorItemRefs[item.type][item.id];
if (anchorRef) anchorRef.current.focus();
} else {
const anchorRef = this.firstAnchorItemRef('folder');
console.info('anchorRef', anchorRef);
if (anchorRef) anchorRef.current.focus();
}
}
} else if (command.name === 'synchronize') {
@@ -417,13 +421,6 @@ class SideBarComponent extends React.Component {
});
}
// searchItem_click(search) {
// this.props.dispatch({
// type: "SEARCH_SELECT",
// id: search ? search.id : null,
// });
// }
async sync_click() {
await shared.synchronize_press(this);
}
@@ -435,6 +432,18 @@ class SideBarComponent extends React.Component {
return this.anchorItemRefs[type][id];
}
firstAnchorItemRef(type) {
const refs = this.anchorItemRefs[type];
if (!refs) return null;
const n = `${type}s`;
const item = this.props[n] && this.props[n].length ? this.props[n][0] : null;
console.info('props', this.props[n], item);
if (!item) return null;
return refs[item.id];
}
noteCountElement(count) {
return <div style={this.style().noteCount}>({count})</div>;
}
@@ -590,8 +599,6 @@ class SideBarComponent extends React.Component {
return { type: 'folder', id: this.props.selectedFolderId };
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
return { type: 'tag', id: this.props.selectedTagId };
} else if (this.props.notesParentType === 'Search' && this.props.selectedSearchId) {
return { type: 'search', id: this.props.selectedSearchId };
}
return null;

View File

@@ -16,7 +16,7 @@ class StatusScreenComponent extends React.Component {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.resfreshScreen();
}

View File

@@ -16,7 +16,7 @@ class TagListComponent extends React.Component {
style.fontSize = theme.fontSize;
const tagItems = [];
if (tags || tags.length > 0) {
if (tags && tags.length > 0) {
// Sort by id for now, but probably needs to be changed in the future.
tags.sort((a, b) => {
return a.title < b.title ? -1 : +1;
@@ -31,10 +31,6 @@ class TagListComponent extends React.Component {
}
}
if (tagItems.length === 0) {
style.visibility = 'hidden';
}
return (
<div className="tag-list" style={style}>
{tagItems}

View File

@@ -1,5 +1,5 @@
const React = require('react');
const electron = require('electron');
const { bridge } = require('electron').remote.require('./bridge');
class VerticalResizer extends React.PureComponent {
constructor() {
@@ -32,7 +32,7 @@ class VerticalResizer extends React.PureComponent {
event.dataTransfer.dropEffect = 'none';
const cursor = electron.screen.getCursorScreenPoint();
const cursor = bridge().screen().getCursorScreenPoint();
this.setState({
drag: {
@@ -52,7 +52,7 @@ class VerticalResizer extends React.PureComponent {
// const e = event.nativeEvent;
// if (!e.buttons || (!e.clientX && !e.clientY && !e.screenX && !e.screenY)) return;
const cursor = electron.screen.getCursorScreenPoint();
const cursor = bridge().screen().getCursorScreenPoint();
const newX = cursor.x;
const delta = newX - this.state.drag.lastX;
if (!delta) return;

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<head id="joplin-container-root-head">
<meta charset="UTF-8">
<style>
@@ -65,19 +65,7 @@
ipc[callName](callData);
}
}));
const loadedCssFiles_ = {};
function loadCssFiles(cssFiles) {
for (let i = 0; i < cssFiles.length; i++) {
const f = cssFiles[i];
if (loadedCssFiles_[f]) continue;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = f;
document.getElementById('joplin-container-styleContainer').appendChild(link);
}
}
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
@@ -154,8 +142,12 @@
}, 1);
}
loadCssFiles(event.options.cssFiles);
if (event.options.pluginAssetsHeadersHtml) {
const pluginAssetsContainer = document.createElement('div');
pluginAssetsContainer.innerHTML = event.options.pluginAssetsHeadersHtml;
document.getElementById('joplin-container-styleContainer').appendChild(pluginAssetsContainer);
}
if (event.options.downloadResources === 'manual') {
webviewLib.setupResourceManualDownload();
}

View File

@@ -0,0 +1,75 @@
/*
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}
.hljs-keyword, .hljs-operator {
color: #F92672;
}
.hljs-pattern-match {
color: #F92672;
}
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #A6E22E;
}
.hljs-function .hljs-params .hljs-typing {
color: #FD971F;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9CCC65;
}
.hljs-comment, .hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag, .hljs-formula {
color: #c678dd;
}
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
color: #98c379;
}
.hljs-built_in, .hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
color: #d19a66;
}
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@@ -0,0 +1,96 @@
/*
Atom One Light by Daniel Gamage
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
base: #fafafa
mono-1: #383a42
mono-2: #686b77
mono-3: #a0a1a7
hue-1: #0184bb
hue-2: #4078f2
hue-3: #a626a4
hue-4: #50a14f
hue-5: #e45649
hue-5-2: #c91243
hue-6: #986801
hue-6-2: #c18401
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #a626a4;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #50a14f;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #c18401;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #986801;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #4078f2;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ const eventManager = require('../../eventManager');
const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('../../InteropServiceHelper.js');
const Note = require('lib/models/Note');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const { substrWithEllipsis } = require('lib/string-utils');
class NoteListUtils {
@@ -49,6 +50,28 @@ class NoteListUtils {
})
);
if (props.watchedNoteFiles.indexOf(noteIds[0]) < 0) {
menu.append(
new MenuItem({
label: _('Edit in external editor'),
enabled: noteIds.length === 1,
click: async () => {
this.startExternalEditing(noteIds[0]);
},
})
);
} else {
menu.append(
new MenuItem({
label: _('Stop external editing'),
enabled: noteIds.length === 1,
click: async () => {
this.stopExternalEditing(noteIds[0]);
},
})
);
}
if (noteIds.length <= 1) {
menu.append(
new MenuItem({
@@ -107,6 +130,20 @@ class NoteListUtils {
})
);
menu.append(
new MenuItem({
label: _('Share note...'),
click: async () => {
console.info('NOTE IDS', noteIds);
props.dispatch({
type: 'WINDOW_COMMAND',
name: 'commandShareNoteDialog',
noteIds: noteIds.slice(),
});
},
})
);
const exportMenu = new Menu();
const ioService = new InteropService();
@@ -114,6 +151,7 @@ class NoteListUtils {
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type !== 'exporter') continue;
if (noteIds.length > 1 && module.canDoMultiExport === false) continue;
exportMenu.append(
new MenuItem({
@@ -133,6 +171,7 @@ class NoteListUtils {
props.dispatch({
type: 'WINDOW_COMMAND',
name: 'exportPdf',
noteId: noteIds[0],
});
},
})
@@ -176,6 +215,20 @@ class NoteListUtils {
if (!ok) return;
await Note.batchDelete(noteIds);
}
static async startExternalEditing(noteId) {
try {
const note = await Note.load(noteId);
ExternalEditWatcher.instance().openAndWatch(note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}
}
static async stopExternalEditing(noteId) {
ExternalEditWatcher.instance().stopWatching(noteId);
}
}
module.exports = NoteListUtils;

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