1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

428 Commits

Author SHA1 Message Date
Laurent Cozic
322ec2efa1 Android release v1.0.100 2018-02-18 22:58:37 +00:00
Laurent Cozic
1232661b1e Electron release v1.0.66 2018-02-18 22:56:57 +00:00
Laurent Cozic
c46d123503 All: Fixed: Local items were no longer being deleted via sync. Also fixed Nextcloud sync target. 2018-02-18 21:52:07 +00:00
Laurent Cozic
8f4060999f Merge branch 'master' of github.com:laurent22/joplin 2018-02-18 21:16:26 +00:00
Laurent Cozic
0addd86069 iOS: Improved compatbility with some WebDAV services (Seafile) 2018-02-18 21:16:18 +00:00
Laurent Cozic
fc6558a64c Electron release v1.0.65 2018-02-17 19:29:26 +00:00
Laurent Cozic
eca500880d All: Convert new lines in tables to BR tag, and added support for HTML tags in Markdown viewers 2018-02-17 19:29:10 +00:00
Laurent Cozic
90bcd7c977 Merge pull request #226 from fmrtn/master
Nextcloud typos and Spanish update
2018-02-17 11:05:25 +00:00
Fernando
cca0c6eaf3 Updated Spanish translation 2018-02-17 01:09:32 +01:00
Fernando
b0736002be Fixed several Nextcloud typos 2018-02-17 01:09:00 +01:00
Laurent Cozic
51fc2d8e51 CLI v1.0.98 2018-02-16 23:49:22 +00:00
Laurent Cozic
d87c192ff1 Updated readme and translation 2018-02-16 23:26:31 +00:00
Laurent Cozic
52ccf398a6 Update readme 2018-02-16 23:17:55 +00:00
Laurent Cozic
344d0e2687 Update readme 2018-02-16 23:14:26 +00:00
Laurent Cozic
1bc4d6b423 CLI v1.0.97 2018-02-16 23:08:57 +00:00
Laurent Cozic
baa9ca7ea3 Updated terminal readme 2018-02-16 23:08:04 +00:00
Laurent Cozic
e4d477fb4c CLI: Resolve #63: Allow customizing shortcuts 2018-02-16 22:53:53 +00:00
Laurent Cozic
71319eee28 Also add link to po file in translation table 2018-02-16 21:45:28 +00:00
Laurent Cozic
68b31526f8 Updated translations 2018-02-16 21:39:54 +00:00
Laurent Cozic
0b2b7324d9 Merge pull request #224 from fmrtn/master
Fixed and updated Spanish translation
2018-02-16 20:35:46 +00:00
Fernando
43512cf27b Fixed and updated Spanish translation 2018-02-16 20:36:58 +01:00
Laurent Cozic
4218b65969 Electron: Added several keyboard shortcuts 2018-02-16 18:08:02 +00:00
Laurent Cozic
7244e12b78 Electron: Fixed: Confirmation message boxes, and release notes text 2018-02-16 18:06:02 +00:00
Laurent Cozic
a796ef5c66 All: Fixed: Notify DecryptionWorker when item added due to conflict, to make sure it is decrypted as early as possible 2018-02-16 17:55:50 +00:00
Laurent Cozic
9474a05aaa Update readme 2018-02-16 00:17:05 +00:00
Laurent Cozic
41df355a7e CLI v1.0.96 2018-02-15 23:51:00 +00:00
Laurent Cozic
4f3ab87914 Electron release v1.0.64 2018-02-15 23:21:47 +00:00
Laurent Cozic
5d1a08707c Android release v1.0.98 2018-02-15 23:20:28 +00:00
Laurent Cozic
4f822df80e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 23:05:34 +00:00
Laurent Cozic
951be5cbf6 Electron: Fixes #201, Fixes #216: Make sure only one update check can run at a time, and improved modal dialog boxes 2018-02-15 23:05:04 +00:00
Laurent Cozic
b6c2341542 Merge pull request #219 from fmrtn/patch-1
Update README.md
2018-02-15 18:33:48 +00:00
Laurent Cozic
a6e6b49a9d Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:33:21 +00:00
Laurent Cozic
3a4bbd571e All: Provide Content-Length header for WebDAV for better compatibility with more servers 2018-02-15 18:33:08 +00:00
Laurent Cozic
feccc6150e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:19:11 +00:00
Laurent Cozic
a37b599a6b macOS: Allow copy and paste from config and encryption screen 2018-02-15 18:19:00 +00:00
Laurent Cozic
9347683fe3 All: Fixed Nextcloud sync target ID (which was incorrectly set to WebDAV sync ID) 2018-02-15 18:01:05 +00:00
Laurent Cozic
3551c26e28 Fixed race condition when testing with memory driver and fixed encoding issue 2018-02-15 17:12:09 +00:00
Fernando
cfca0107eb Update README.md 2018-02-15 13:09:59 +01:00
Laurent Cozic
81bc975193 Update readme 2018-02-14 21:54:34 +00:00
Laurent Cozic
7908fda451 Android release v1.0.97 2018-02-14 19:11:35 +00:00
Laurent Cozic
cdbb7c4b0d Merge branch 'master' of github.com:laurent22/joplin 2018-02-14 19:10:42 +00:00
Laurent Cozic
414e57ec55 Electron release v1.0.63 2018-02-14 19:08:24 +00:00
Laurent Cozic
1871123066 All: Improved WebDAV driver compatibility with some services (eg. Seafile) 2018-02-14 19:08:07 +00:00
Laurent Cozic
87bc08bef5 iOS: Fixed resource decryption issue, log page crash and text input rendering issue 2018-02-14 15:28:56 +00:00
Laurent Cozic
214a39c3d3 All: Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken. 2018-02-13 18:26:33 +00:00
Laurent Cozic
ef0cc5e33e Update readme 2018-02-12 20:23:16 +00:00
Laurent Cozic
3a1fa583ab CLI v1.0.95 2018-02-12 18:07:20 +00:00
Laurent Cozic
c1161ae017 Android release v1.0.95 2018-02-12 17:58:55 +00:00
Laurent Cozic
1023ec6206 Electron release v1.0.62 2018-02-12 17:56:42 +00:00
Laurent Cozic
7841421c0d All: Fixes #209: Items with non-ASCII characters end up truncated on Nextcloud 2018-02-12 18:15:22 +00:00
Laurent Cozic
995d8c35dd Updated readme 2018-02-11 20:49:28 +00:00
Laurent Cozic
b179471eff Electron: Fixes #205: Importing Evernote notes while on import page re-imports previous import 2018-02-11 20:31:26 +00:00
Laurent Cozic
19a126ebfe Android release v1.0.94 2018-02-11 17:29:09 +00:00
Laurent Cozic
7e56e5b587 Mobile: Fixes #207: Crash when changing dropdown value in config screen 2018-02-11 17:26:20 +00:00
Laurent Cozic
acf0c79341 Graduated E2EE and WebDAV from beta, and moved to v1.0 2018-02-11 13:50:59 +00:00
Laurent Cozic
9fe7e23ffe Android release v0.10.92 2018-02-11 13:20:32 +00:00
Laurent Cozic
c94cc93971 Fixed translator names 2018-02-10 13:03:01 +00:00
Laurent Cozic
b26094eba8 Fixed Basque flag 2018-02-10 12:52:57 +00:00
Laurent Cozic
89a5ccdf93 Added Basque translation, fixed issue with handling invalid translations. Updated translation FR. 2018-02-10 12:43:45 +00:00
Laurent Cozic
ce2da0e6dc Update readme 2018-02-09 16:03:43 +00:00
Laurent Cozic
f49d644b6a Electron release v0.10.61 2018-02-08 18:15:49 +00:00
Laurent Cozic
02ac0b8593 Removed uneeded created_time property 2018-02-07 20:42:52 +00:00
Laurent Cozic
78e5eaf1e2 Electron: Toolbar button to set tags 2018-02-07 20:35:11 +00:00
Laurent Cozic
fc0d227396 Electron: Allowing opening and saving resource images 2018-02-07 20:23:17 +00:00
Laurent Cozic
f91c52cdf7 Mobile: Update time when app is activated 2018-02-07 19:51:58 +00:00
Laurent Cozic
3f14878d0f All: Improved request repeating mechanism 2018-02-07 19:46:07 +00:00
Laurent Cozic
69fd32e7c6 All: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time 2018-02-07 19:02:07 +00:00
Laurent Cozic
80801cedf0 Android release v0.10.91 2018-02-07 17:57:08 +00:00
Laurent Cozic
480e4fa94b Merge branch 'master' of github.com:laurent22/joplin 2018-02-07 17:56:36 +00:00
Laurent Cozic
717c789836 Android release v0.10.90 2018-02-07 17:55:54 +00:00
Laurent Cozic
f099376446 Update README.md 2018-02-07 17:55:17 +00:00
Laurent Cozic
41fa9d093e Android release v0.10.89 2018-02-07 17:54:06 +00:00
Laurent Cozic
e2f3f81eb6 Android release v0.10.88 2018-02-07 17:51:31 +00:00
Laurent Cozic
5cab7aeb55 Fixed: Make sure alarms and resources are attached to right note when creating new note 2018-02-06 19:31:22 +00:00
Laurent Cozic
fa5f418c22 All: Added sync config check to config screens 2018-02-06 18:59:36 +00:00
Laurent Cozic
a25fcacace Electron: Display message when creating new note or to-do 2018-02-06 18:12:43 +00:00
Laurent Cozic
727ba7300e All: Also support $ as delimiter for Katex expressoins 2018-02-06 17:58:54 +00:00
Laurent Cozic
d25d9b3f44 CLI v0.10.93 2018-02-06 13:17:10 +00:00
Laurent Cozic
9d762a4319 Android release v0.10.86 2018-02-06 13:01:20 +00:00
Laurent Cozic
18d94c7585 Electron release v0.10.60 2018-02-06 12:57:40 +00:00
Laurent Cozic
af82345eb8 Fixed tray icon and update issue 2018-02-06 13:11:59 +00:00
Laurent Cozic
1e94a22986 Merge branch 'master' of github.com:laurent22/joplin 2018-02-06 13:06:59 +00:00
Laurent Cozic
e19a8a99ff macOS: Fixed startup crash 2018-02-06 13:05:36 +00:00
Laurent Cozic
f975009e24 Update readme 2018-02-05 19:02:11 +00:00
Laurent Cozic
90640fafc7 Merge branch 'master' of github.com:laurent22/joplin 2018-02-05 18:49:58 +00:00
Laurent Cozic
42e0e1e5a5 Updated build instructions 2018-02-06 09:42:20 +00:00
Laurent Cozic
61f64fa933 Added Markdown doc 2018-02-05 22:53:10 +00:00
Laurent Cozic
0d0ffd6d27 Added Markdown doc 2018-02-05 22:50:17 +00:00
Laurent Cozic
023ccffd2e Electron release v0.10.59 2018-02-05 22:20:31 +00:00
Laurent Cozic
bc26098c7d Android release v0.10.85 2018-02-05 22:19:21 +00:00
Laurent Cozic
7257a71a18 Mobile: Fixes #159: Make sure text fields aren't hidden by keyboard on iOS 2018-02-05 18:32:59 +00:00
Laurent Cozic
8ad8b73585 Better error handling for Katex and handling of code blocks and inline 2018-02-05 17:55:35 +00:00
Laurent Cozic
9a06815db9 Electron release v0.10.58 2018-02-05 17:34:39 +00:00
Laurent Cozic
66947d4954 Fixing appveyor script 2018-02-05 17:34:31 +00:00
Laurent Cozic
3ec22185d5 Electron release v0.10.57 2018-02-05 17:32:54 +00:00
Laurent Cozic
0f05c23e26 Fixing deployment script 2018-02-05 17:32:48 +00:00
Laurent Cozic
74493fece0 Android release v0.10.83 2018-02-05 17:32:00 +00:00
Laurent Cozic
557a96e814 Electron release v0.10.56 2018-02-05 17:27:38 +00:00
Laurent Cozic
4b23b419a4 Electron release v0.10.55 2018-02-05 17:27:09 +00:00
Laurent Cozic
8b7f5b1151 Fix scrolling issue in Electron app 2018-02-04 18:45:52 +00:00
Laurent Cozic
29e9ccf216 Electron: Reverted to older Sharp version to fix Ubuntu issue 2018-02-04 18:31:13 +00:00
Laurent Cozic
2c04f5c8bc Improved Android and Electron release process 2018-02-04 18:05:07 +00:00
Laurent Cozic
5430a747e9 Improved Android and Electron release process 2018-02-04 17:55:22 +00:00
Laurent Cozic
13bc185829 Improved Android and Electron release process 2018-02-04 17:51:42 +00:00
Laurent Cozic
ed87581a8a Improved Android and Electron release process 2018-02-04 17:48:29 +00:00
Laurent Cozic
2645ec96a8 Fixed tool utils 2018-02-04 17:44:32 +00:00
Laurent Cozic
d278d830f0 Improved Android and Electron release process 2018-02-04 17:42:33 +00:00
Laurent Cozic
b4dce0ed46 All: Added Katex support 2018-02-04 17:12:24 +00:00
Laurent Cozic
e8416042d4 Merge branch 'master' into math-support 2018-02-02 20:35:32 +00:00
Laurent Cozic
70adbe5e76 Added flags 2018-02-01 20:21:54 +00:00
Laurent Cozic
f66be08d1d Added list of translated languages to README file 2018-02-01 20:15:31 +00:00
Laurent Cozic
fad96f5266 All: Added section to list missing master keys 2018-02-01 19:01:20 +00:00
Laurent Cozic
c33a7f5f47 Updated French translation 2018-02-02 00:08:37 +00:00
Laurent Cozic
28afbcde02 Updated translations 2018-02-02 00:02:47 +00:00
Laurent Cozic
691292d2b3 Merge branch 'master' of github.com:laurent22/joplin 2018-02-01 23:40:15 +00:00
Laurent Cozic
30ff81064f All: Made WebDAV driver more generic to support services other than Nextcloud and added WebDAV sync target 2018-02-01 23:40:05 +00:00
Laurent Cozic
f9f398ad98 Merge pull request #194 from rtmkrlv/russian-locale
Update Russian translation
2018-02-01 15:26:15 +01:00
rtmkrlv
537884bdcd Merge branch 'master' into russian-locale 2018-02-01 15:50:39 +02:00
rtmkrlv
d54400a7cb Updated Russian translation 2018-02-01 15:43:26 +02:00
Laurent Cozic
42c78264fb Update website 2018-01-31 20:21:38 +00:00
Laurent Cozic
c52da82447 Electron: Fix: Don't allow adding notes and to-do to conflict notebook 2018-01-31 20:19:11 +00:00
Laurent Cozic
cca43624e4 Electron: Added tray icon support 2018-01-31 20:10:32 +00:00
Laurent Cozic
dac1cd7668 Mobile: Allow filtering log by warning/error 2018-01-31 19:51:29 +00:00
Laurent Cozic
b4c00db0e3 Electron release v0.10.54 2018-01-31 19:40:40 +00:00
Laurent Cozic
3ce393a8b2 Electron release v0.10.53 2018-01-31 19:34:47 +00:00
Laurent Cozic
2b627fe4ab Fixed auto-update check 2018-01-31 19:34:38 +00:00
Laurent Cozic
fcf8a1649d CLI v0.10.92 2018-01-31 19:28:31 +00:00
Laurent Cozic
8d3b050831 Update website 2018-01-31 19:26:21 +00:00
Laurent Cozic
43297ef0a3 Updated translations 2018-01-31 19:14:32 +00:00
Laurent Cozic
551fabdfc9 Tweak and error handling on auto-update 2018-01-31 19:10:45 +00:00
Laurent Cozic
d6de56b2db All: Fixed crash when having invalid UTF-8 string in text editor 2018-01-31 19:01:11 +00:00
Laurent Cozic
9e979804f3 Electron release v0.10.52 2018-01-31 17:53:18 +00:00
Laurent Cozic
b8e0f182cc Android release v0.10.81 2018-01-31 17:51:22 +00:00
Laurent Cozic
9a41b9e192 Electron: Improved auto-update process to avoid random crashes 2018-01-30 22:35:50 +00:00
Laurent Cozic
9b8f520b9f Electron: Allow focusing either title or body when creating a new note or to-odo 2018-01-30 21:49:22 +00:00
Laurent Cozic
5b6019805c Electron: Fixed auto-title when title is manually entered first 2018-01-30 21:36:54 +00:00
Laurent Cozic
a4106436c4 Fixed delta function when processing many items 2018-01-30 21:10:54 +00:00
Laurent Cozic
f6b4eb511e Add Content-Size header for WebDAV, which is required by some services 2018-01-30 20:24:09 +00:00
Laurent Cozic
eb67ac17a0 Allow decryption to continue even if an item cannot be decrypted 2018-01-30 20:15:05 +00:00
Laurent Cozic
7b760d03ef All: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only) 2018-01-30 20:10:36 +00:00
Laurent Cozic
2805ae2acf Fixed crash when calling fetch() with invalid URL in RN app 2018-01-30 19:01:07 +00:00
Laurent Cozic
5cb5ccc781 All: Optimised Nextcloud sync delta functionality 2018-01-29 20:51:14 +00:00
Laurent Cozic
0dba2821b6 Merge pull request #189 from strobeltobias/patch-2
Update German translation
2018-01-29 18:59:20 +00:00
Tobias Strobel
1db7825b22 Update German translation
Grammar and wording fixes, translating new strings.
2018-01-29 14:24:50 +01:00
Laurent Cozic
8a92d6ad70 Update website 2018-01-28 18:47:22 +00:00
Laurent Cozic
138ad9fcad Mobile: Fixes #114: Update geolocation in metadata after it has been set 2018-01-28 18:42:43 +00:00
Laurent Cozic
08cb518c25 Check if current folder exists 2018-01-28 18:19:56 +00:00
Laurent Cozic
6d04eab200 Merge pull request #185 from pf-siedler/autocomp_file_and_item
Autocompletion for files and items
2018-01-28 18:17:01 +00:00
Laurent Cozic
8a8cb51e1b Electron release v0.10.51 2018-01-28 18:07:08 +00:00
Laurent Cozic
5c66042a2d CLI v0.10.91 2018-01-28 17:59:34 +00:00
Laurent Cozic
ae75181b02 Electron release v0.10.50 2018-01-28 17:52:15 +00:00
Laurent Cozic
9dc3238182 Added beta notice 2018-01-28 17:51:38 +00:00
Laurent Cozic
0a68749373 Android release v0.10.79 2018-01-28 17:45:54 +00:00
Laurent Cozic
1519116291 Updated French translation 2018-01-28 17:43:21 +00:00
Laurent Cozic
d023d841e2 Update translations 2018-01-28 17:38:30 +00:00
Laurent Cozic
d7a1465d8e Skip sync report events in log 2018-01-28 17:38:17 +00:00
Laurent Cozic
15848fc696 Closing a resource is async 2018-01-28 17:37:51 +00:00
Laurent Cozic
837ae2c9f2 Send only one NOT_LOADED event per master key 2018-01-28 17:37:29 +00:00
Laurent Cozic
6789b98ead Return fileNotFound error when file cannot be opened so that it is skipped by synchroniser 2018-01-28 17:37:03 +00:00
Laurent Cozic
29f6e74ee3 Convert fs errors to normal errors 2018-01-28 17:36:36 +00:00
Laurent Cozic
2780c38c45 Fixed WebDAV error handling 2018-01-28 17:36:11 +00:00
Laurent Cozic
4531838217 Display stars for secure config value 2018-01-28 17:35:20 +00:00
pf-siedler
7bccf7f65d Implement auto completaion for <item> usage 2018-01-28 23:12:54 +09:00
pf-siedler
c62a24a9cb Implement auto completion for <file> usage
Suggest file names from current directory.
2018-01-28 22:59:58 +09:00
Laurent Cozic
c6830499f7 Fixed Travis 2018-01-28 11:48:29 +00:00
Laurent Cozic
d9f00a2539 Electron release v0.10.49 2018-01-26 17:21:34 +00:00
Laurent Cozic
def83c9119 Merge branch 'master' of github.com:laurent22/joplin 2018-01-26 17:19:58 +00:00
Laurent Cozic
b6cb0056c7 CLI v0.10.90 2018-01-26 17:19:42 +00:00
Laurent Cozic
1669b5258a Fixed detection of encrypted item 2018-01-25 23:01:18 +00:00
Laurent Cozic
5a9e0bfc26 Handle password text input in mobile and desktop 2018-01-25 22:44:09 +00:00
Laurent Cozic
8f3fdb3afe Tweaks to make sure Nextcloud driver passes all test units 2018-01-25 21:15:58 +00:00
Laurent Cozic
7ab135c099 Various tweaks to get Nextcloud working in mobile 2018-01-25 20:48:01 +00:00
Laurent Cozic
1cc27f2509 Got Nextcloud sync to work in Electron 2018-01-25 19:01:14 +00:00
Laurent Cozic
ef700b421c Update CONTRIBUTING.md 2018-01-25 13:35:04 +00:00
Laurent Cozic
b9af5ac052 Update issue_template.md 2018-01-25 13:34:07 +00:00
Laurent Cozic
173f2d421d Update PULL_REQUEST_TEMPLATE 2018-01-25 13:32:19 +00:00
Laurent Cozic
9f82c069c9 Update CONTRIBUTING.md 2018-01-25 13:31:17 +00:00
Laurent Cozic
6ade09c228 Merge branch 'master' into webdav 2018-01-24 17:25:34 +00:00
Laurent Cozic
5393a1399c Finished WebDAV driver 2018-01-23 20:10:20 +00:00
Laurent Cozic
fd29f20b2e Electron: Fix checkbox issue in config screen 2018-01-23 18:31:49 +00:00
Laurent Cozic
c011b53d1f Electron: Upgraded Electron to 1.7.11 to fix security vulnerability 2018-01-23 18:10:30 +00:00
Laurent Cozic
26e3a7b68c Merge branch 'master' of github.com:laurent22/joplin 2018-01-23 17:53:19 +00:00
Laurent Cozic
e70a291698 Merge pull request #174 from gabcoh/fix171
Fix #171
2018-01-23 11:26:11 +00:00
Laurent Cozic
511bd57726 Merge pull request #175 from alexdevero/add-font-size-settings
Add font size settings
2018-01-23 11:25:47 +00:00
Laurent Cozic
c6de8598dc Electron release v0.10.48 2018-01-22 19:10:29 +00:00
Laurent Cozic
7bee25599d Removed uneeded code 2018-01-22 19:06:50 +00:00
Laurent Cozic
773a1ad829 Travis: only build tags 2018-01-21 20:03:40 +00:00
Laurent Cozic
1a1e264fa4 All: Refactored so that memory and file sync target use same delta logic 2018-01-21 19:45:32 +00:00
Laurent Cozic
5b99ecefca Merge branch 'master' into webdav 2018-01-21 19:10:39 +00:00
Laurent Cozic
1bfeed377a All: Optimised file sync logic so that it doesn't fetch the content of
all the items on each sync. Also limit the number of items in a batch
to 1000
2018-01-21 18:54:47 +00:00
Laurent Cozic
86eee376bb All: Handle case where resource blob is missing during sync 2018-01-21 17:48:50 +00:00
Laurent Cozic
6a7d368184 All: Started Nextcloud support 2018-01-21 17:01:37 +00:00
Alex Devero
1da19ae98d Fix indentation 2018-01-19 14:11:40 +01:00
Alex Devero
f52c117b09 Add font size settings 2018-01-19 13:27:44 +01:00
Laurent Cozic
2551f96149 Fixed Readme 2018-01-18 22:43:37 +00:00
Laurent Cozic
c984c19fee Android release v0.10.78 2018-01-18 22:35:05 +00:00
Laurent Cozic
ac8e91e82e Started FAQ 2018-01-18 22:34:27 +00:00
Gabe Cohen
af50d80541 Fix #171 2018-01-18 14:29:13 -06:00
Laurent Cozic
e355f4e49b Fixed license 2018-01-18 20:14:05 +00:00
Laurent Cozic
738ef2b0fa Android release v0.10.75 2018-01-18 17:29:57 +00:00
Laurent Cozic
9746a3964b All: Removed certain log statements so that sensitive info doesn't end up in logs 2018-01-17 21:17:40 +00:00
Laurent Cozic
9efbf74b6f All: Various changes to get filesystem target to work on mobile 2018-01-17 21:01:41 +00:00
Laurent Cozic
c16ea6b237 Typo 2018-01-17 20:19:45 +00:00
Laurent Cozic
b06a3b588f Add APK download link 2018-01-17 20:19:20 +00:00
Laurent Cozic
6ff67e0995 Automate building and deploying Android app 2018-01-17 20:16:13 +00:00
Laurent Cozic
1a5c8d126d All: Refactored filesystem sync driver to support mobile 2018-01-17 18:51:15 +00:00
Laurent Cozic
f632580eed CLI: Display error when cannot open note with editor 2018-01-17 18:10:07 +00:00
Laurent Cozic
1d73f0cdee Simplified and fixed caching issue 2018-01-17 17:59:33 +00:00
Laurent Cozic
99c7111f8c CLI: Fixes #168: Invalid code block would crash app 2018-01-17 17:52:55 +00:00
Laurent Cozic
ae9806561a Added tech spec info 2018-01-16 19:51:37 +00:00
Laurent Cozic
fffdf5b5b7 Fixed link 2018-01-17 00:47:45 +00:00
Laurent Cozic
3de19c3db7 All: Added Dutch translation. Thanks @tcassaert 2018-01-17 00:43:47 +00:00
Laurent Cozic
56e074b4ef Merge branch 'master' of github.com:laurent22/joplin 2018-01-17 00:41:04 +00:00
Laurent Cozic
1a79253780 Update doc 2018-01-17 00:40:35 +00:00
Laurent Cozic
b67908df11 Merge pull request #165 from tcassaert/master
Dutch translation
2018-01-16 17:32:33 +00:00
tcassaert
6a5089f71d Dutch translation
All EN keys are translated to nl_BE.
2018-01-15 22:33:05 +01:00
Laurent Cozic
f710463b67 Electron: Fixes #155: Caret alignment issue with Russian text 2018-01-15 19:01:00 +00:00
Laurent Cozic
6ae0c3aba0 Electron release v0.10.47 2018-01-15 18:41:19 +00:00
Laurent Cozic
07c6347014 Android v0.10.74 2018-01-15 18:40:44 +00:00
Laurent Cozic
b10999e83e All: Update French translation 2018-01-15 18:35:39 +00:00
Laurent Cozic
961b5bfd25 All: Fixes #85: Don't record deleted_items entries for folders deleted via sync 2018-01-15 18:10:14 +00:00
Laurent Cozic
d1f1d1068a Electron release v0.10.46 2018-01-15 12:29:58 +00:00
Laurent Cozic
faade0afe2 All: Fixed model ID issue 2018-01-14 17:11:44 +00:00
Laurent Cozic
a442a49e2f Electron release v0.10.45 2018-01-15 12:27:10 +00:00
Laurent Cozic
7d3fbbcaba Updated translations 2018-01-14 17:07:34 +00:00
Laurent Cozic
d9bb7c3271 Android v0.10.73 2018-01-14 17:05:59 +00:00
Laurent Cozic
4d1dd17fa2 All: Fixed issue with timestamp when saving notes 2018-01-14 17:01:22 +00:00
Laurent Cozic
c5c6c777be Electron release v0.10.44 2018-01-12 18:11:39 +01:00
Laurent Cozic
1fd1a73fda Electron: Improved the way new note are created, and automatically add a title. Made saving and loading notes more reliable. 2018-01-12 19:58:01 +00:00
Laurent Cozic
feeb498a79 All: Fixed OneDrive sync when resync is requested 2018-01-12 19:01:34 +00:00
Laurent Cozic
1d7f30d441 Electron: Fixed logic to save, and make sure scheduled save always happen even when changing note 2018-01-11 21:05:34 +01:00
Laurent Cozic
53da63e371 Trying to add math support 2018-01-11 19:51:01 +00:00
Laurent Cozic
424443a2d8 CLI: Display arrays and objects in settings 2018-01-09 21:25:31 +01:00
Laurent Cozic
08b58f0e4c All: Fixed table font size and family 2018-01-09 21:09:49 +01:00
Laurent Cozic
c2a0d8600f Electron: Move prompt to top to avoid issue with date picker being hidden 2018-01-09 21:06:47 +01:00
Laurent Cozic
ede3c2ce2f Electron: Fixed display of too long notebook titles 2018-01-09 19:34:06 +01:00
Laurent Cozic
0b93515711 Electron: Display URL for links 2018-01-09 19:26:46 +01:00
Laurent Cozic
2f13e689b9 Electron: Don't scroll back to top when note is reloaded via sync 2018-01-09 20:26:20 +00:00
Laurent Cozic
ea135a0d28 Electron: Fixed logic of what note is used when right-clicking one or more notes 2018-01-09 20:16:09 +00:00
Laurent Cozic
f67e4a03e4 CLI: Detect installed Node version 2018-01-09 19:56:38 +00:00
Laurent Cozic
e9268edeff All: Fixes #150: Extra comma causes crash 2018-01-09 19:45:08 +00:00
Laurent Cozic
a8576a55d6 CLI v0.10.87 2018-01-09 09:31:03 +01:00
Laurent Cozic
eb500cdf9e All: Display sync items being fetched 2018-01-08 21:36:00 +01:00
Laurent Cozic
7b9dc66121 All: Schedule sync after enabling or disabling encryption 2018-01-08 21:25:38 +01:00
Laurent Cozic
bba2c68c6f All: Schedule sync only after 30 seconds 2018-01-08 21:05:08 +01:00
Laurent Cozic
c70d8bea78 All: Fixes #129: Tags are case insensitive 2018-01-08 21:04:44 +01:00
Laurent Cozic
176bda66ad Merge branch 'master' of github.com:laurent22/joplin 2018-01-08 20:09:12 +01:00
Laurent Cozic
78ce10ddf0 All: Fixed race condition when a note is being uploaded while it's being modified in the text editor 2018-01-08 20:09:01 +01:00
Laurent Cozic
29f14681a8 Mobile: Fixed mix of tabs and spaces 2018-01-08 19:31:04 +00:00
Laurent Cozic
aaf617e41c iOS v0.10.9 2018-01-08 18:26:30 +01:00
Laurent Cozic
b99146ed7f Merge pull request #111 from marcosvega91/fix_scroll_note_keyboard
Fix scroll note keyboard on IOS
2018-01-08 16:45:58 +00:00
Laurent Cozic
d136161650 Android v0.10.71 2018-01-07 19:29:57 +00:00
Laurent Cozic
39051a27a1 Update website 2018-01-08 11:12:22 +01:00
Laurent Cozic
30bc9dd551 Electron release v0.10.43 2018-01-08 10:57:36 +01:00
Laurent Cozic
cc2f665313 Electron: Undone tests 2018-01-08 10:57:29 +01:00
Laurent Cozic
37c989ed28 Electron release v0.10.42 2018-01-08 10:27:02 +01:00
Laurent Cozic
adc5885980 All: Better handling of null values in settings 2018-01-07 19:20:10 +00:00
Laurent Cozic
8de5b4219d All: Updated translations 2018-01-06 21:05:34 +01:00
Laurent Cozic
e096ddebd4 Merge branch 'master' of github.com:laurent22/joplin 2018-01-06 20:40:47 +01:00
Laurent Cozic
83398dd0bc Mobile: Fixes #139: Crash when creating new notebook 2018-01-06 20:37:42 +01:00
Laurent Cozic
ddc78ebb41 Merge pull request #138 from rtmkrlv/russian-locale
Updated Russian translation
2018-01-06 19:34:02 +00:00
rtmkrlv
70b69eb31e Merge branch 'master' into russian-locale 2018-01-06 14:59:28 +02:00
rtmkrlv
3fa891e136 Merge branch 'master' into russian-locale 2018-01-06 14:54:46 +02:00
rtmkrlv
6f7a9f3295 Updated Russian translation
Added translation of new strings and small corrections
2018-01-06 14:45:30 +02:00
Laurent Cozic
8e8793943b Update README.md 2018-01-06 10:19:50 +01:00
Laurent Cozic
83d9faf2fe Create PULL_REQUEST_TEMPLATE 2018-01-06 10:17:14 +01:00
Laurent Cozic
f45a4fff8b Mobile: Fixes #136, fixes #137: styling issues on E2EE 2018-01-06 10:06:41 +01:00
Laurent Cozic
1e02aa3120 Update website 2018-01-05 21:38:37 +01:00
Laurent Cozic
77fec75f23 CLI v0.10.86 2018-01-05 21:02:23 +01:00
Laurent Cozic
2c9feae20b Merge branch 'master' of github.com:laurent22/joplin 2018-01-05 20:52:41 +01:00
Laurent Cozic
a81788b3fa Electron release v0.10.41 2018-01-05 20:52:22 +01:00
Laurent Cozic
88cfba53a3 Update CONTRIBUTING.md 2018-01-05 20:50:28 +01:00
Laurent Cozic
d659d975cd All: Documentation and minor tweaks for E2EE 2018-01-05 18:40:57 +01:00
marcosvega91
277ad90f72 Indent with tab 2017-12-19 21:14:40 +01:00
marcosvega91
f2e3bedde6 Fix scroll
After fixing the issue on ios, it caused an issue on android that I solved with this commit
2017-12-19 10:28:52 +01:00
marcosvega91
98c0f2315a Fix scroll
Fixed the issue that not permit to view edited text when the keyboard is shown.
2017-12-19 10:08:22 +01:00
Laurent Cozic
0926755635 Create README_debugging.md 2017-12-17 18:54:29 +01:00
Laurent Cozic
75f8234db5 All: Added Russian translation. Thanks @rtmkrlv 2017-12-17 12:34:56 +01:00
Laurent Cozic
b8e85e1587 Merge pull request #106 from rtmkrlv/russian-locale
Russian localisation
2017-12-17 11:33:34 +00:00
Laurent Cozic
b6add857f9 Merge branch 'master' of github.com:laurent22/joplin 2017-12-17 12:30:47 +01:00
Laurent Cozic
4e41731c08 All: Updated translations 2017-12-17 12:30:32 +01:00
rtmkrlv
6ac3b8a8f9 Add files via upload
Russian localisation for Joplin. 100% done, but some corrections may be required.
2017-12-16 20:41:04 +02:00
Laurent Cozic
7ab2b11c9d Update README.md
Updated list of supported languages
2017-12-15 10:07:55 +00:00
Laurent Cozic
1eb1c5914c Merge branch 'master' of github.com:laurent22/joplin 2017-12-15 08:46:39 +00:00
Laurent Cozic
5780951f7d Updated translations 2017-12-15 08:46:30 +00:00
Laurent Cozic
67e790e393 Merge pull request #95 from tbroadley/fix-typos
Fix typos
2017-12-15 07:21:26 +00:00
Thomas Broadley
0d472486ca Fix typos 2017-12-14 19:10:47 -05:00
Laurent Cozic
2d218904d0 Merge branch 'master' of github.com:laurent22/joplin 2017-12-11 21:20:57 +00:00
Laurent Cozic
c3fbcb8feb Update website 2017-12-11 21:20:49 +00:00
Laurent Cozic
64031ac6cf Merge pull request #40 from Cogitri/german-locale
WIP: Add German (Germany) locale
2017-12-11 20:43:07 +00:00
Laurent Cozic
caae7f7cf8 CLI v0.10.83 2017-12-11 20:12:41 +00:00
Laurent Cozic
d56b247adf Electron release v0.10.39 2017-12-11 20:09:38 +00:00
Laurent Cozic
11bdfbde61 Fixed Readme typo 2017-12-10 16:44:43 +00:00
Laurent Cozic
a59bf55c16 Electron: Fixes #3: Paths with '.' would cause JSX compilation to fail 2017-12-10 16:06:43 +00:00
Laurent Cozic
043be1916c CLI: Display welcome message the first time the app is run. 2017-12-10 15:56:12 +00:00
Laurent Cozic
42e34b5c3b All: Fixes #87: Show warningn when deleting notebook that contains notes. 2017-12-10 14:09:12 +00:00
Laurent Cozic
931083b2e2 Electron: Fixes #86: App icon missing on Linux 2017-12-10 14:04:07 +00:00
Rasmus Thomsen
09b9df4228 German locale: adjust articulation 2017-12-07 16:02:18 +01:00
Laurent Cozic
37663bd110 Merge pull request #68 from dbinary/master
adding translate spanish Latin America
2017-12-06 23:59:22 +00:00
Laurent Cozic
f01c6aa8d1 Merge branch 'master' of github.com:laurent22/joplin 2017-12-06 23:56:18 +00:00
Laurent Cozic
8838017830 All: Updated translations 2017-12-06 23:55:22 +00:00
Laurent Cozic
1d6fb8058f Update BUILD.md 2017-12-06 23:34:56 +00:00
Laurent Cozic
bb51729bea Update BUILD.md 2017-12-06 23:34:31 +00:00
Laurent Cozic
507e7e6014 All: Improved ENEX import for web pages that have been saved as notes 2017-12-06 19:29:58 +00:00
Laurent Cozic
f42908b11c Merge branch 'sync-limits' 2017-12-05 17:30:16 +00:00
Laurent Cozic
03ec406627 All: Filter to sync target and refactored so that same code can be used by all clients 2017-12-05 19:21:01 +00:00
Laurent Cozic
c703521b6c All: Handling of unsyncable items 2017-12-05 18:56:39 +00:00
Laurent Cozic
cf97bf9a77 Updated readme 2017-12-05 08:47:48 +00:00
Laurent Cozic
304b9a582f Electron release v0.10.36 2017-12-04 23:57:30 +00:00
Laurent Cozic
4d5c4b1743 Electron: started unsynchronisable items UI 2017-12-04 23:57:13 +00:00
Laurent Cozic
4abe5d07c4 All: Handling of impossible-to-sync items (such as when they are over the size limit of the cloud provider) 2017-12-04 23:38:09 +00:00
Laurent Cozic
f6633e23f5 Minor tweaks 2017-12-04 23:01:22 +00:00
Laurent Cozic
a6d6201ecb All: Improved synchronisation process and saving of models so that reducer can deal with full objects 2017-12-04 22:58:42 +00:00
Laurent Cozic
0115e74163 All: Minor tweaks regarding encryption 2017-12-04 19:29:34 +00:00
Luis Pérez
4314c392f6 adding translate spanish Latin America 2017-12-04 13:24:55 -06:00
Laurent Cozic
685f541bb4 All: Fixed issue with local resource needlessly marked as encrypted 2017-12-04 19:15:14 +00:00
Laurent Cozic
f9634ea283 CLI: Added e2ee command to check encryption status of a sync target 2017-12-04 19:01:56 +00:00
Laurent Cozic
6252a4d8c8 Merge branch 'master' into encryption 2018-01-03 20:23:26 +01:00
Laurent Cozic
d13d7bec45 Minor changes 2018-01-03 20:20:16 +01:00
Laurent Cozic
74e284d795 Merge branch 'master' of github.com:laurent22/joplin 2018-01-03 00:17:39 +01:00
Laurent Cozic
7b82dacbf2 Update website 2018-01-03 00:17:31 +01:00
Laurent Cozic
f82162f94a Create CONTRIBUTING.md
Started "contributing" guide
2018-01-02 22:04:41 +01:00
Laurent Cozic
e2d5128624 Update README.md
More information on how to import notes into Joplin
2018-01-02 21:51:18 +01:00
Laurent Cozic
9074412472 Merge pull request #128 from tanrax/master
Update README with Importing from Standard Notes
2018-01-02 21:46:13 +01:00
Laurent Cozic
0db9c66317 Electron release v0.10.40 2018-01-02 21:28:56 +01:00
Laurent Cozic
b22211fada All: Updated translations. Added Japanese and Chinese. 2018-01-02 21:27:38 +01:00
Laurent Cozic
7efeaa3a22 All: Fixed minor issue when logger not defined 2018-01-02 21:23:35 +01:00
Laurent Cozic
79efac2f71 Merge pull request #130 from SamuelBlickle/master
Improved german translation
2018-01-02 21:22:27 +01:00
Laurent Cozic
9d7b7092f5 Merge pull request #132 from kcrt/master
Japanese localization
2018-01-02 21:22:14 +01:00
Laurent Cozic
71e877d369 All: Improved encryption and synchronisation 2018-01-02 20:17:14 +01:00
TAKAHASHI Kyohei
7e086a7730 Japanese localization 2018-01-02 14:17:39 +09:00
SamuelBlickle
3f810c71b0 improved german translations 2018-01-01 13:41:02 +01:00
Laurent Cozic
500fbc5294 Merge branch 'master' into encryption 2017-12-31 15:26:16 +01:00
Laurent Cozic
6d0f60d9a1 All: Fixed tests and minor fixes 2017-12-31 15:23:05 +01:00
Laurent Cozic
e19c26fdd1 Mobile: Done UI for E2EE 2017-12-31 14:58:50 +01:00
Andros Fenollosa
86b86513d9 Update README with Importing from Standard Notes 2017-12-30 21:37:37 +01:00
Laurent Cozic
6ff19063ef All: Got E2EE working in mobile app 2017-12-30 20:57:34 +01:00
Laurent Cozic
6a75451539 Merge pull request #120 from marcosvega91/fix_undo_note
Fix #119
2017-12-29 19:42:48 +01:00
Laurent Cozic
70a33f8533 CLI 0.10.84 2017-12-29 11:17:07 +01:00
Laurent Cozic
23722719f0 Merge pull request #82 from gabcoh/autocomplete
Added auto complete
2017-12-29 10:54:38 +01:00
Laurent Cozic
d499251206 Update app-gui.js
Invalidate root after inputting command
2017-12-29 10:39:50 +01:00
Laurent Cozic
d180e7b5e1 Mobile: Handle encrypted items on UI side 2017-12-28 19:57:21 +00:00
Laurent Cozic
9e869a5b1f CLI: Various UI improvements for E2EE 2017-12-28 19:51:24 +00:00
Laurent Cozic
ab959623aa CLI: Fixes #117: 'edit' command raising error in shell mode 2017-12-28 19:14:03 +00:00
Laurent Cozic
08d2655f13 All: Various improvements to E2EE 2017-12-26 11:38:53 +01:00
Gabe Cohen
20632ae1c1 Correctly implement autocomplete menu with prefix 2017-12-24 12:27:25 -06:00
Laurent Cozic
bef7c38724 Electron: Improved enabling/disabling E2EE 2017-12-24 11:23:51 +01:00
Laurent Cozic
d1abf4971d Electron: E2EE config 2017-12-24 09:36:31 +01:00
Laurent Cozic
d13c2cf8d7 All: Fixed reading file by chuncks 2017-12-23 21:21:11 +01:00
Gabe Cohen
3146273409 Improve autocomplete
All commands and sub commands should now autocomplete. Titles are now wrapped in spaces and quoted titles are now properly parsed.
2017-12-22 19:48:56 -06:00
Laurent Cozic
1891eb4998 All: Upgraded to fs-extra 5.0 2017-12-22 18:58:09 +00:00
Laurent Cozic
70b03971f6 All: Fixed handling of unloaded master key 2017-12-22 18:50:27 +00:00
marcosvega91
38c050b47e Removed duplicate editor variable 2017-12-22 15:43:01 +01:00
marcosvega91
0bf5c9ebdd Fix #119
Fix the bug that permit to undo changes from different notes.
I save the editor instance into the state and in the componentWillReceiveProps i reset the undo state
2017-12-22 14:48:44 +01:00
Laurent Cozic
6683da804b All: Fixed various issues regarding encryption and decryptino of resources, and started doing GUI for Electron app 2017-12-21 20:06:08 +01:00
Laurent Cozic
7750b954fc All: Make sure existing resources are encrypted once E2EE has been enabled 2017-12-20 19:43:43 +00:00
Laurent Cozic
edbff5a26a All: Simplified encryption header 2017-12-20 19:14:27 +00:00
Laurent Cozic
18846c11ed All: Allow disabling encryption and added more test cases 2017-12-20 20:45:25 +01:00
Laurent Cozic
cc02c1d585 All: Simplified synchronisation of resources to simplify encryption, and implemented resource encryption 2017-12-19 19:01:29 +00:00
Laurent Cozic
26bf7c4d46 All: Improved file and memory drivers to make it easier to use same sync method for all the drivers 2017-12-18 21:47:25 +01:00
Laurent Cozic
2959fa1080 All: Added uploadBlob method to Node apps 2017-12-18 21:46:22 +01:00
Laurent Cozic
3f4f154949 All: Refactored encryption/decryption method to use same algorithm for both file and string encryption 2017-12-18 20:54:03 +01:00
Laurent Cozic
4c0b472f67 All: Testing and better handling of E2EE initialisation 2017-12-17 20:51:45 +01:00
Gabe Cohen
4756238821 Remove unused import 2017-12-15 06:39:50 -06:00
Laurent Cozic
f5d26e0d81 All: Fixed filesystem driver bug when downloading resources. Added support for encrypting all items when encryption enabled. 2017-12-14 21:12:02 +00:00
Laurent Cozic
e9bb5bee9d All: Better handling of encrypted data on UI side and prevent modification of encrypted notes 2017-12-14 20:21:36 +00:00
Laurent Cozic
2c608bca3c All: getting encryption service and UI to work 2017-12-14 19:39:13 +00:00
Laurent Cozic
d9c1e30e9b Merge master into encryption branch 2017-12-15 07:31:57 +00:00
Laurent Cozic
5bc72e2b44 All: Decryption worker and handling of missing master key passwords 2017-12-14 18:53:08 +00:00
Laurent Cozic
df05d04dad All: Made model naming more consistent 2017-12-14 18:12:14 +00:00
Laurent Cozic
888ac8f4c2 Electron: Started integrating encryption 2017-12-14 17:58:10 +00:00
Laurent Cozic
55266e5694 Fixed test units 2017-12-14 17:41:53 +00:00
Gabe Cohen
39c73e1649 Improve autocompletion
1. Removed autocomplete menu because it lists the entire line, not just what is
being autocompleted.

2. Autocomplete positional args first unless cursor is at - then autocomplete
long options

3. Don't autocomplete an options that is already present.

4. Other fixes
2017-12-14 07:53:49 -06:00
Gabe Cohen
3bf9d01f0a Remove unused code and retab 2017-12-14 07:01:00 -06:00
Gabe Cohen
89ef33f7ca convert spaces to tabs 2017-12-13 21:25:18 -06:00
Gabe Cohen
f71fe9a1a6 Make autocomplete more intelligent 2017-12-13 21:13:43 -06:00
Laurent Cozic
1008b1835b All: Handle tag encryption and started CLI and Electron encryption front-end 2017-12-14 00:23:32 +00:00
Laurent Cozic
2ffa5419e2 Merge branch 'encryption' of github.com:laurent22/joplin into encryption 2017-12-13 22:53:30 +00:00
Laurent Cozic
bd20ecff78 All: Handle conflict for encrypted notes 2017-12-13 22:53:20 +00:00
Laurent Cozic
a073514c46 Update README_spec.md 2017-12-13 22:38:47 +00:00
Laurent Cozic
c6ff14226f Fixed readme 2017-12-13 22:37:11 +00:00
Laurent Cozic
ee02f8255e All: Encryption setup on apps 2017-12-13 19:01:04 +00:00
Laurent Cozic
5951ed3f55 All: Support encrypting notes and notebooks 2017-12-13 18:57:40 +00:00
Laurent Cozic
f6fbf3ba0f All: Handle master key in backend 2017-12-12 21:58:57 +00:00
Laurent Cozic
9bce52a92a All: Utilities to create and encrypt master keys 2017-12-12 18:41:02 +00:00
Laurent Cozic
e44975622a CLI: Handling of password prompt 2017-12-12 18:17:30 +00:00
Laurent Cozic
92b857d83b All: Added methods to get secure random bytes 2017-12-12 17:51:07 +00:00
Gabe Cohen
671e538740 Contain autocomplete in StatusBarWidget.js
StatusBarWidget.js now imports app itself and gets command names.
app-gui.js no longer sets the autocomplete of StatusBarWidget itself.
2017-12-11 19:31:11 -06:00
Laurent Cozic
8a282fd2e1 All: Added fs drivers and encryption service 2017-12-11 23:52:42 +00:00
Gabe Cohen
cda623a95c Added command auto complete
File based autocompletion is not yet implemented. This will require knowledge of
the command, and it's parameters. The autocomplete feture is pretty powerful
however, so this should not be very difficult to add.
2017-12-09 23:08:28 -06:00
Laurent Cozic
0f343bccda Electron: resolve #7: Show storage location in Options screen 2017-12-08 21:51:59 +00:00
Laurent Cozic
a513f6f3f0 Merge branch 'master' of github.com:laurent22/joplin 2017-12-08 21:42:37 +00:00
Laurent Cozic
3b4809714e iOS: version 0.10.6 2017-12-08 21:42:09 +00:00
Laurent Cozic
238b5ab9b9 CLI: version bump to fix Homebrew formula 2017-12-08 21:41:41 +00:00
Laurent Cozic
08d9e9b6aa All: Added support for HTML tags found in ENEX files: colgroup, col, ins, kbd, address, caption, var, area, map 2017-12-08 17:41:32 +00:00
Laurent Cozic
b22900eb3a Update website 2017-12-08 10:12:52 +00:00
Laurent Cozic
89fc2c4779 Removed uneeded assets 2017-12-07 23:29:47 +00:00
Laurent Cozic
353b79f5e5 Electron release v0.10.38 2017-12-07 23:25:06 +00:00
Laurent Cozic
b929b46281 Mobile: Fixed dropdown positioning 2017-12-07 23:24:14 +00:00
Laurent Cozic
5c4a536dad Android v0.10.65 2017-12-07 23:11:34 +00:00
Laurent Cozic
91e337307c Mobile: Upgrade react-native-popup-menu to fix scrolling bug 2017-12-07 23:00:22 +00:00
Laurent Cozic
2855b68ed4 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 22:56:41 +00:00
Laurent Cozic
45ca6284f9 iOS: Added safe area for iPhone X support 2017-12-07 22:42:40 +00:00
Laurent Cozic
145ee13356 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 22:42:08 +00:00
Laurent Cozic
6f97747199 Electron: Add support for file system sync 2017-12-07 22:29:02 +00:00
Laurent Cozic
f3751e4ba6 Mobile: Fixed auto-assignment of title for new notes 2017-12-07 21:32:22 +00:00
Laurent Cozic
5cd55cada6 Mobile: Made context menu scrollable 2017-12-07 21:23:41 +00:00
Laurent Cozic
bad4b2ecb8 Electron: Added dialog to export sync status 2017-12-07 21:18:18 +00:00
Laurent Cozic
7aafd63ff3 Update website 2017-12-07 19:38:24 +00:00
Laurent Cozic
3227a13035 Electron release v0.10.37 2017-12-07 18:00:28 +00:00
Laurent Cozic
027f96d100 Electron: Fixes #36: Set installation directory for Windows installer 2017-12-07 18:17:41 +00:00
Laurent Cozic
f242a3c215 Merge branch 'fix-cli' 2017-12-07 17:43:44 +00:00
Laurent Cozic
ad6c347180 CLI: Fixed shell mode 2017-12-07 18:12:46 +00:00
Laurent Cozic
4f0431da55 Merge branch 'master' of github.com:laurent22/joplin 2017-12-07 13:33:26 +00:00
Laurent Cozic
b873fdd029 iOS: Release 2017-12-07 17:46:59 +00:00
Laurent Cozic
c1ff820913 Electron: Fixes #65: Display 'no notebook' message on startup. 2017-12-07 13:16:38 +00:00
Laurent Cozic
7008daf92a All: Improved handling of empty links when importing ENEX files. Fixed minor layout issues in Electron app 2017-12-07 00:57:36 +00:00
Laurent Cozic
ed914c6907 All: Improved rendering of imported ENEX tables 2017-12-07 00:35:15 +00:00
Laurent Cozic
73e81a54b4 All: Fixed sync issue when target does not have reliable timestamps 2017-12-04 18:39:40 +00:00
Laurent Cozic
ab8c66a361 Updated tools 2017-12-04 18:16:14 +00:00
Laurent Cozic
4b55fefcb1 Cleaned up build tools 2017-12-04 17:55:06 +00:00
Laurent Cozic
0eac8b25e1 Deleted uneeded file 2017-12-03 23:06:02 +00:00
Laurent Cozic
aec556ff7d Delete package-lock.json 2017-12-03 23:03:16 +00:00
Laurent Cozic
110dc29bd4 Merge pull request #57 from mgroth0/patch-1
Add node-gyp dependency to instructions
2017-12-03 21:45:40 +00:00
Laurent Cozic
b1efea1bd9 Update BUILD.md 2017-12-03 21:44:04 +00:00
Laurent Cozic
8671467ed3 Update BUILD.md 2017-12-03 21:43:55 +00:00
Matt Groth
485ef1f2c2 Add node-gyp dependency to instructions
I was getting an error on `yarn dist` which I solved with the command `npm install -g node-gyp`.

This was the error:
```
Error: /usr/local/Cellar/node/8.9.1/bin/node exited with code 1
Output:
$ node-pre-gyp install --fallback-to-build
Failed to execute 'node-gyp clean' (Error: spawn node-gyp ENOENT)
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Error output:
node-pre-gyp info it worked if it ends with ok
node-pre-gyp info using node-pre-gyp@0.6.38
node-pre-gyp info using node@8.9.1 | darwin | x64
node-pre-gyp http GET https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp http 403 https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! Tried to download(403): https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! Pre-built binaries not found for sqlite3@3.1.13 and electron@1.7.9 (electron-v1.7 ABI) (falling back to source compile with node-gyp)
node-pre-gyp http 403 status code downloading tarball https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v3.1.13/electron-v1.7-darwin-x64.tar.gz
node-pre-gyp ERR! build error
node-pre-gyp ERR! stack Error: Failed to execute 'node-gyp clean' (Error: spawn node-gyp ENOENT)
node-pre-gyp ERR! stack     at ChildProcess.<anonymous> (/Users/matt/joplin/ElectronClient/app/node_modules/sqlite3/node_modules/node-pre-gyp/lib/util/compile.js:77:29)
node-pre-gyp ERR! stack     at emitOne (events.js:116:13)
node-pre-gyp ERR! stack     at ChildProcess.emit (events.js:211:7)
node-pre-gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:196:12)
node-pre-gyp ERR! stack     at onErrorNT (internal/child_process.js:372:16)
node-pre-gyp ERR! stack     at _combinedTickCallback (internal/process/next_tick.js:138:11)
node-pre-gyp ERR! stack     at process._tickCallback (internal/process/next_tick.js:180:9)
node-pre-gyp ERR! System Darwin 16.7.0
node-pre-gyp ERR! command "/usr/local/Cellar/node/8.9.1/bin/node" "/Users/matt/joplin/ElectronClient/app/node_modules/sqlite3/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build"
node-pre-gyp ERR! cwd /Users/matt/joplin/ElectronClient/app/node_modules/sqlite3
node-pre-gyp ERR! node -v v8.9.1
node-pre-gyp ERR! node-pre-gyp -v v0.6.38
node-pre-gyp ERR! not ok
error Command failed with exit code 1.

    at ChildProcess.childProcess.once.code (/Users/matt/joplin/ElectronClient/app/node_modules/builder-util/src/util.ts:219:14)
    at Object.onceWrapper (events.js:317:30)
    at emitTwo (events.js:126:13)
    at ChildProcess.emit (events.js:214:7)
    at maybeClose (internal/child_process.js:925:16)
    at Socket.stream.socket.on (internal/child_process.js:346:11)
    at emitOne (events.js:116:13)
    at Socket.emit (events.js:211:7)
    at Pipe._handle.close [as _onclose] (net.js:554:12)
From previous event:
    at spawn (/Users/matt/joplin/ElectronClient/app/node_modules/builder-util/src/util.ts:182:3)
    at default.map.concurrency (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:154:7)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:152:27
From previous event:
    at rebuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/util/yarn.js:94:22)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/util/yarn.ts:21:11
    at Generator.next (<anonymous>)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at installOrRebuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/util/yarn.js:32:21)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:388:7
    at Generator.next (<anonymous>)
From previous event:
    at Packager.installAppDependencies (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:433:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:284:20
    at Generator.next (<anonymous>)
From previous event:
    at Packager.doBuild (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:369:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/packager.ts:236:52
    at Generator.next (<anonymous>)
From previous event:
    at Packager.build (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/packager.js:298:11)
    at /Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/builder.ts:277:40
    at Generator.next (<anonymous>)
From previous event:
    at build (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/out/builder.js:63:21)
    at then (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:49:4)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)
From previous event:
    at Object.args [as handler] (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:49:4)
    at Object.runCommand (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/lib/command.js:228:22)
    at Object.parseArgs [as _parseArgs] (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/yargs.js:1041:24)
    at Object.get [as argv] (/Users/matt/joplin/ElectronClient/app/node_modules/yargs/yargs.js:957:21)
    at Object.<anonymous> (/Users/matt/joplin/ElectronClient/app/node_modules/electron-builder/src/cli/cli.ts:43:15)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3
```
2017-12-01 21:28:13 -05:00
Laurent Cozic
b9194e94aa Electron: Fixes #84: Fields losing focus in Config screen 2017-11-08 11:41:23 +00:00
278 changed files with 27153 additions and 4734 deletions

7
.gitignore vendored
View File

@@ -35,4 +35,9 @@ _vieux/
_mydocs
.DS_Store
Assets/DownloadBadges*.psd
node_modules
node_modules
Tools/github_oauth_token.txt
_releases
ReactNativeClient/lib/csstojs/
ElectronClient/app/gui/note-viewer/fonts/
Tools/commit_hook.txt

View File

@@ -1,3 +1,6 @@
# Only build tags (Doesn't work - doesn't build anything)
if: tag IS present
rvm: 2.3.3
matrix:
@@ -43,7 +46,8 @@ before_install:
script:
- |
cd ElectronClient/app
rsync -aP ../../ReactNativeClient/lib/ lib/
cd Tools
npm install
yarn dist
cd ../ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && yarn dist

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
#!/bin/bash
iconutil --convert icns macOs.iconset

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg>

Before

Width:  |  Height:  |  Size: 612 B

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"/></svg>

Before

Width:  |  Height:  |  Size: 447 B

View File

@@ -1,2 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@@ -1,38 +1,61 @@
# General information
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when builing each app.
- The translations are built by running CliClient/build-translation.sh. For this reasons, it's generally better to get the CLI app to build first so that everything is setup correctly.
- 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.
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
## macOS dependencies
brew install yarn node xgettext
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
brew install yarn node
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
## Linux and Windows dependencies
## 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/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
Before building any of the applications, you need to build the tools:
```
cd Tools
npm install
```
# Building the Electron application
```
cd ElectronClient/app
rsync -a ../../ReactNativeClient/lib/ lib/
rsync --delete -a ../../ReactNativeClient/lib/ lib/
npm install
yarn dist
```
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
For node-gyp to work, you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
That will create the executable file in the `dist` directory.
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
# Building the Mobile application
From `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
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 "Building Projects with Native Code" tab.
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
# Building the Terminal application
From `/CliClient`, run `npm install` then run `run.sh`. If you get an error about `xgettext`, comment out the command `node build-translation.js --silent` in build.sh
```
cd CliClient
npm install
./build.sh
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
```
Run `run.sh` to start the application for testing.

20
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,20 @@
# Reporting a bug
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/README_debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
# Feature requests
Again, please check that it has not already been requested. If it has, simply **up-vote the issue** - the ones with the most up-votes are likely to be implemented. Adding a "+1" comment does nothing.
# Adding new features
If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
# Coding style
- Only use tabs for indentation, not spaces.
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.

View File

@@ -1,6 +1,6 @@
const { _ } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const { Resource } = require('lib/models/resource.js');
const Resource = require('lib/models/Resource.js');
const { netUtils } = require('lib/net-utils.js');
const http = require("http");

View File

@@ -1,9 +1,9 @@
const { Logger } = require('lib/logger.js');
const { Folder } = require('lib/models/folder.js');
const { Tag } = require('lib/models/tag.js');
const { BaseModel } = require('lib/base-model.js');
const { Note } = require('lib/models/note.js');
const { Resource } = require('lib/models/resource.js');
const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const { cliUtils } = require('./cli-utils.js');
const { reducer, defaultState } = require('lib/reducer.js');
const { splitCommandString } = require('lib/string-utils.js');
@@ -14,6 +14,7 @@ const chalk = require('chalk');
const tk = require('terminal-kit');
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
const Renderer = require('tkwidgets/framework/Renderer.js');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseWidget = require('tkwidgets/BaseWidget.js');
const ListWidget = require('tkwidgets/ListWidget.js');
@@ -34,37 +35,55 @@ const ConsoleWidget = require('./gui/ConsoleWidget.js');
class AppGui {
constructor(app, store) {
this.app_ = app;
this.store_ = store;
constructor(app, store, keymap) {
try {
this.app_ = app;
this.store_ = store;
BaseWidget.setLogger(app.logger());
BaseWidget.setLogger(app.logger());
this.term_ = new TermWrapper(tk.terminal);
this.term_ = new TermWrapper(tk.terminal);
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
// Some keys are directly handled by the tkwidget framework
// so they need to be remapped in a different way.
this.tkWidgetKeys_ = {
'focus_next': 'TAB',
'focus_previous': 'SHIFT_TAB',
'move_up': 'UP',
'move_down': 'DOWN',
'page_down': 'PAGE_DOWN',
'page_up': 'PAGE_UP',
};
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.renderer_ = null;
this.logger_ = new Logger();
this.buildUi();
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.shortcuts_ = this.setupShortcuts();
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.keymap_ = this.setupKeymap(keymap);
this.commandCancelCalled_ = false;
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
this.commandCancelCalled_ = false;
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
DecryptionWorker.instance().scheduleStart();
} catch (error) {
this.fullScreen(false);
console.error(error);
process.exit(1);
}
}
store() {
@@ -80,8 +99,16 @@ class AppGui {
await this.renderer_.renderRoot();
}
prompt(initialText = '', promptString = ':') {
return this.widget('statusBar').prompt(initialText, promptString);
termSaveState() {
return this.term().saveState();
}
termRestoreState(state) {
return this.term().restoreState(state);
}
prompt(initialText = '', promptString = ':', options = null) {
return this.widget('statusBar').prompt(initialText, promptString, options);
}
stdoutMaxWidth() {
@@ -95,6 +122,7 @@ class AppGui {
buildUi() {
this.rootWidget_ = new ReduxRootWidget(this.store_);
this.rootWidget_.name = 'root';
this.rootWidget_.autoShortcutsEnabled = false;
const folderList = new FolderListWidget();
folderList.style = {
@@ -262,152 +290,26 @@ class AppGui {
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupShortcuts() {
const shortcuts = {};
setupKeymap(keymap) {
const output = [];
shortcuts['TAB'] = {
friendlyName: 'Tab',
description: () => _('Give focus to next pane'),
isDocOnly: true,
}
for (let i = 0; i < keymap.length; i++) {
const item = Object.assign({}, keymap[i]);
shortcuts['SHIFT_TAB'] = {
friendlyName: 'Shift+Tab',
description: () => _('Give focus to previous pane'),
isDocOnly: true,
}
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
shortcuts[':'] = {
description: () => _('Enter command line mode'),
action: async () => {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
await this.processCommand(cmd);
},
};
if (!('type' in item)) item.type = 'exec';
shortcuts['ESC'] = { // Built into terminal-kit inputField
description: () => _('Exit command line mode'),
isDocOnly: true,
};
shortcuts['ENTER'] = {
description: () => _('Edit the selected note'),
action: () => {
const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') {
this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') {
this.processCommand('edit $n');
}
},
}
shortcuts['CTRL_C'] = {
description: () => _('Cancel the current command.'),
friendlyName: 'Ctrl+C',
isDocOnly: true,
}
shortcuts['CTRL_D'] = {
description: () => _('Exit the application.'),
friendlyName: 'Ctrl+D',
isDocOnly: true,
}
shortcuts['DELETE'] = {
description: () => _('Delete the currently selected note or notebook.'),
action: async () => {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
this.stdout(_('To delete a tag, untag the associated notes.'));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
type: 'SEARCH_DELETE',
id: item.id,
});
}
} else if (this.widget('noteList').hasFocus) {
await this.processCommand('rmnote $n');
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
if (item.command in this.tkWidgetKeys_) {
item.type = 'tkwidgets';
}
};
shortcuts['BACKSPACE'] = {
alias: 'DELETE',
};
item.canRunAlongOtherCommands = item.type === 'function' && ['toggle_metadata', 'toggle_console'].indexOf(item.command) >= 0;
shortcuts[' '] = {
friendlyName: 'SPACE',
description: () => _('Set a to-do as completed / not completed'),
action: 'todo toggle $n',
output.push(item);
}
shortcuts['tc'] = {
description: () => _('[t]oggle [c]onsole between maximized/minimized/hidden/visible.'),
action: () => {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
},
canRunAlongOtherCommands: true,
}
shortcuts['/'] = {
description: () => _('Search'),
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
};
shortcuts['tm'] = {
description: () => _('[t]oggle note [m]etadata.'),
action: () => {
this.toggleNoteMetadata();
},
canRunAlongOtherCommands: true,
}
shortcuts['mn'] = {
description: () => _('[M]ake a new [n]ote'),
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
}
shortcuts['mt'] = {
description: () => _('[M]ake a new [t]odo'),
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
}
shortcuts['mb'] = {
description: () => _('[M]ake a new note[b]ook'),
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
}
shortcuts['yn'] = {
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
}
shortcuts['dn'] = {
description: () => _('Move the note to a notebook.'),
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
}
return shortcuts;
return output;
}
toggleConsole() {
@@ -482,8 +384,16 @@ class AppGui {
return this.logger_;
}
shortcuts() {
return this.shortcuts_;
keymap() {
return this.keymap_;
}
keymapItemByKey(key) {
for (let i = 0; i < this.keymap_.length; i++) {
const item = this.keymap_[i];
if (item.keys.indexOf(key) >= 0) return item;
}
return null;
}
term() {
@@ -514,18 +424,78 @@ class AppGui {
}
}
async processCommand(cmd) {
async processFunctionCommand(cmd) {
if (cmd === 'activate') {
const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') {
this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') {
this.processPromptCommand('edit $n');
}
} else if (cmd === 'delete') {
if (this.widget('folderList').hasFocus) {
const item = this.widget('folderList').selectedJoplinItem;
if (!item) return;
if (item.type_ === BaseModel.TYPE_FOLDER) {
await this.processPromptCommand('rmbook ' + item.id);
} else if (item.type_ === BaseModel.TYPE_TAG) {
this.stdout(_('To delete a tag, untag the associated notes.'));
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store().dispatch({
type: 'SEARCH_DELETE',
id: item.id,
});
}
} else if (this.widget('noteList').hasFocus) {
await this.processPromptCommand('rmnote $n');
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
} else if (cmd === 'toggle_console') {
if (!this.consoleIsShown()) {
this.showConsole();
this.minimizeConsole();
} else {
if (this.consoleIsMaximized()) {
this.hideConsole();
} else {
this.maximizeConsole();
}
}
} else if (cmd === 'toggle_metadata') {
this.toggleNoteMetadata();
} else if (cmd === 'enter_command_line_mode') {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.addCommandToConsole(cmd);
await this.processPromptCommand(cmd);
} else {
throw new Error('Unknown command: ' + cmd);
}
}
async processPromptCommand(cmd) {
if (!cmd) return;
cmd = cmd.trim();
if (!cmd.length) return;
this.logger().info('Got command: ' + cmd);
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
await this.app().exit();
return;
}
try {
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
@@ -548,6 +518,10 @@ class AppGui {
}
this.widget('console').scrollBottom();
// Invalidate so that the screen is redrawn in case inputting a command has moved
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
this.widget('root').invalidate();
}
async updateFolderList() {
@@ -772,35 +746,34 @@ class AppGui {
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
let cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
let keymapItem = this.keymapItemByKey(shortcutKey);
// If this command is an alias to another command, resolve to the actual command
if (cmd && cmd.alias) cmd = this.shortcuts_[cmd.alias];
let processShortcutKeys = !this.app().currentCommand() && cmd;
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
if (statusBar.promptActive) processShortcutKeys = false;
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
if (processShortcutKeys) {
this.logger().info('Shortcut:', shortcutKey, cmd.description());
this.logger().info('Shortcut:', shortcutKey, keymapItem);
this.currentShortcutKeys_ = [];
if (typeof cmd.action === 'function') {
await cmd.action();
} else if (typeof cmd.action === 'object') {
if (cmd.action.type === 'prompt') {
let promptOptions = {};
if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processCommand(commandString);
} else {
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
}
} else { // String
this.stdout(cmd.action);
await this.processCommand(cmd.action);
if (keymapItem.type === 'function') {
this.processFunctionCommand(keymapItem.command);
} else if (keymapItem.type === 'prompt') {
let promptOptions = {};
if ('cursorPosition' in keymapItem) promptOptions.cursorPosition = keymapItem.cursorPosition;
const commandString = await statusBar.prompt(keymapItem.command ? keymapItem.command : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processPromptCommand(commandString);
} else if (keymapItem.type === 'exec') {
this.stdout(keymapItem.command);
await this.processPromptCommand(keymapItem.command);
} else if (keymapItem.type === 'tkwidgets') {
this.widget('root').handleKey(this.tkWidgetKeys_[keymapItem.command]);
} else {
throw new Error('Unknown command type: ' + JSON.stringify(keymapItem));
}
}
@@ -826,4 +799,4 @@ class AppGui {
AppGui.INPUT_MODE_NORMAL = 1;
AppGui.INPUT_MODE_META = 2;
module.exports = AppGui;
module.exports = AppGui;

View File

@@ -5,12 +5,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
const { Database } = require('lib/database.js');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { Setting } = require('lib/models/setting.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const Setting = require('lib/models/Setting.js');
const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
@@ -21,6 +21,7 @@ const os = require('os');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const EventEmitter = require('events');
const Cache = require('lib/Cache');
class Application extends BaseApplication {
@@ -34,6 +35,7 @@ class Application extends BaseApplication {
this.allCommandsLoaded_ = false;
this.showStackTraces_ = false;
this.gui_ = null;
this.cache_ = new Cache();
}
gui() {
@@ -144,13 +146,15 @@ class Application extends BaseApplication {
message += ' (' + options.answers.join('/') + ')';
}
let answer = await this.gui().prompt('', message + ' ');
let answer = await this.gui().prompt('', message + ' ', options);
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
} else {
return answer;
}
});
@@ -177,22 +181,33 @@ class Application extends BaseApplication {
await doExit();
}
commands() {
if (this.allCommandsLoaded_) return this.commands_;
commands(uiType = null) {
if (!this.allCommandsLoaded_) {
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
this.allCommandsLoaded_ = true;
}
this.allCommandsLoaded_ = true;
if (uiType !== null) {
let temp = [];
for (let n in this.commands_) {
if (!this.commands_.hasOwnProperty(n)) continue;
const c = this.commands_[n];
if (!c.supportsUi(uiType)) continue;
temp[n] = c;
}
return temp;
}
return this.commands_;
}
@@ -210,12 +225,8 @@ class Application extends BaseApplication {
async commandMetadata() {
if (this.commandMetadata_) return this.commandMetadata_;
const osTmpdir = require('os-tmpdir');
const storage = require('node-persist');
await storage.init({ dir: osTmpdir() + '/commandMetadata', ttl: 1000 * 60 * 60 * 24 });
let output = await storage.getItem('metadata');
if (Setting.value('env') != 'dev' && output) {
let output = await this.cache_.getItem('metadata');
if (output) {
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
}
@@ -229,7 +240,7 @@ class Application extends BaseApplication {
output[n] = cmd.metadata();
}
await storage.setItem('metadata', output);
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
@@ -246,9 +257,13 @@ class Application extends BaseApplication {
try {
CommandClass = require(__dirname + '/command-' + name + '.js');
} catch (error) {
let e = new Error('No such command: ' + name);
e.type = 'notFound';
throw e;
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
let e = new Error(_('No such command: %s', name));
e.type = 'notFound';
throw e;
} else {
throw error;
}
}
let cmd = new CommandClass();
@@ -260,7 +275,7 @@ class Application extends BaseApplication {
dummyGui() {
return {
isDummy: () => { return true; },
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
showConsole: () => {},
maximizeConsole: () => {},
stdout: (text) => { console.info(text); },
@@ -268,7 +283,10 @@ class Application extends BaseApplication {
exit: () => {},
showModalOverlay: (text) => {},
hideModalOverlay: () => {},
stdoutMaxWidth: () => { return 78; }
stdoutMaxWidth: () => { return 78; },
forceRender: () => {},
termSaveState: () => {},
termRestoreState: (state) => {},
};
}
@@ -294,6 +312,63 @@ class Application extends BaseApplication {
return this.activeCommand_;
}
async loadKeymaps() {
const defaultKeyMap = [
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
];
// Filter the keymap item by command so that items in keymap.json can override
// the default ones.
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
}
const filePath = Setting.value('profileDir') + '/keymap.json';
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
const keymap = JSON.parse(configString);
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
const item = keymap[keymapIndex];
itemsByCommand[item.command] = item;
}
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load keymap ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
}
const output = [];
for (let n in itemsByCommand) {
if (!itemsByCommand.hasOwnProperty(n)) continue;
output.push(itemsByCommand[n]);
}
return output;
}
async start(argv) {
argv = await super.start(argv);
@@ -306,6 +381,8 @@ class Application extends BaseApplication {
if (argv.length) {
this.gui_ = this.dummyGui();
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
try {
await this.execCommand(argv);
} catch (error) {
@@ -318,8 +395,10 @@ class Application extends BaseApplication {
} else { // Otherwise open the GUI
this.initRedux();
const keymap = await this.loadKeymaps();
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store());
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
await this.gui_.start();
@@ -334,7 +413,7 @@ class Application extends BaseApplication {
this.dispatch({
type: 'TAG_UPDATE_ALL',
tags: tags,
items: tags,
});
this.store().dispatch({

View File

@@ -0,0 +1,199 @@
var { app } = require('./app.js');
var Note = require('lib/models/Note.js');
var Folder = require('lib/models/Folder.js');
var Tag = require('lib/models/Tag.js');
var { cliUtils } = require('./cli-utils.js');
var yargParser = require('yargs-parser');
var fs = require('fs-extra');
async function handleAutocompletionPromise(line) {
// Auto-complete the command name
const names = await app().commandNames();
let words = getArguments(line);
//If there is only one word and it is not already a command name then you
//should look for commmands it could be
if (words.length == 1) {
if (names.indexOf(words[0]) === -1) {
let x = names.filter((n) => n.indexOf(words[0]) === 0);
if (x.length === 1) {
return x[0] + ' ';
}
return x.length > 0 ? x.map((a) => a + ' ') : line;
} else {
return line;
}
}
//There is more than one word and it is a command
const metadata = (await app().commandMetadata())[words[0]];
//If for some reason this command does not have any associated metadata
//just don't autocomplete. However, this should not happen.
if (metadata === undefined) {
return line;
}
//complete an option
let next = words.length > 1 ? words[words.length - 1] : '';
let l = [];
if (next[0] === '-') {
for (let i = 0; i<metadata.options.length; i++) {
const options = metadata.options[i][0].split(' ');
//if there are multiple options then they will be seperated by comma and
//space. The comma should be removed
if (options[0][options[0].length - 1] === ',') {
options[0] = options[0].slice(0, -1);
}
if (words.includes(options[0]) || words.includes(options[1])) {
continue;
}
//First two elements are the flag and the third is the description
//Only autocomplete long
if (options.length > 1 && options[1].indexOf(next) === 0) {
l.push(options[1]);
} else if (options[0].indexOf(next) === 0) {
l.push(options[0]);
}
}
if (l.length === 0) {
return line;
}
let ret = l.map(a=>toCommandLine(a));
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
return ret;
}
//Complete an argument
//Determine the number of positional arguments by counting the number of
//words that don't start with a - less one for the command name
const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1;
let cmdUsage = yargParser(metadata.usage)['_'];
cmdUsage.splice(0, 1);
if (cmdUsage.length >= positionalArgs) {
let argName = cmdUsage[positionalArgs - 1];
argName = cliUtils.parseCommandArg(argName).name;
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
l.push(...notes.map((n) => n.title));
}
if (argName == 'notebook') {
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...folders.map((n) => n.title));
}
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: next + '*' });
l.push(...tags.map((n) => n.title));
}
if (argName == 'file') {
let files = await fs.readdir('.');
l.push(...files);
}
if (argName == 'tag-command') {
let c = filterList(['add', 'remove', 'list'], next);
l.push(...c);
}
if (argName == 'todo-command') {
let c = filterList(['toggle', 'clear'], next);
l.push(...c);
}
}
if (l.length === 1) {
return toCommandLine([...words.slice(0, -1), l[0]]);
} else if (l.length > 1) {
let ret = l.map(a=>toCommandLine(a));
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
return ret;
}
return line;
}
function handleAutocompletion(str, callback) {
handleAutocompletionPromise(str).then(function(res) {
callback(undefined, res);
});
}
function toCommandLine(args) {
if (Array.isArray(args)) {
return args.map(function(a) {
if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
return "'" + a + "'";
} else if (a.indexOf("'") !== -1) {
return '"' + a + '"';
} else {
return a;
}
}).join(' ');
} else {
if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
return "'" + args + "' ";
} else if (args.indexOf("'") !== -1) {
return '"' + args + '" ';
} else {
return args + ' ';
}
}
}
function getArguments(line) {
let inSingleQuotes = false;
let inDoubleQuotes = false;
let currentWord = '';
let parsed = [];
for(let i = 0; i<line.length; i++) {
if(line[i] === '"') {
if(inDoubleQuotes) {
inDoubleQuotes = false;
//maybe push word to parsed?
//currentWord += '"';
} else {
inDoubleQuotes = true;
//currentWord += '"';
}
} else if(line[i] === "'") {
if(inSingleQuotes) {
inSingleQuotes = false;
//maybe push word to parsed?
//currentWord += "'";
} else {
inSingleQuotes = true;
//currentWord += "'";
}
} else if (/\s/.test(line[i]) &&
!(inDoubleQuotes || inSingleQuotes)) {
if (currentWord !== '') {
parsed.push(currentWord);
currentWord = '';
}
} else {
currentWord += line[i];
}
}
if (!(inSingleQuotes || inDoubleQuotes) && /\s/.test(line[line.length - 1])) {
parsed.push('');
} else {
parsed.push(currentWord);
}
return parsed;
}
function filterList(list, next) {
let output = [];
for (let i = 0; i < list.length; i++) {
if (list[i].indexOf(next) !== 0) continue;
output.push(list[i]);
}
return output;
}
module.exports = { handleAutocompletion };

View File

@@ -12,6 +12,10 @@ class BaseCommand {
throw new Error('Usage not defined');
}
encryptionCheck(item) {
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
}
description() {
throw new Error('Description not defined');
}

View File

@@ -5,10 +5,10 @@ const { Logger } = require('lib/logger.js');
const { dirname } = require('lib/path-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Setting } = require('lib/models/setting.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec

View File

@@ -178,38 +178,39 @@ cliUtils.promptConfirm = function(message, answers = null) {
});
}
cliUtils.promptInput = function(message) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve, reject) => {
rl.question(message + ' ', (answer) => {
rl.close();
resolve(answer);
});
});
}
// Note: initialText is there to have the same signature as statusBar.prompt() so that
// it can be a drop-in replacement, however initialText is not used (and cannot be
// with readline.question?).
cliUtils.prompt = function(initialText = '', promptString = ':') {
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
if (!options) options = {};
const readline = require('readline');
const Writable = require('stream').Writable;
const mutableStdout = new Writable({
write: function(chunk, encoding, callback) {
if (!this.muted)
process.stdout.write(chunk, encoding);
callback();
}
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: mutableStdout,
terminal: true,
});
return new Promise((resolve, reject) => {
mutableStdout.muted = false;
rl.question(promptString, (answer) => {
rl.close();
if (!!options.secure) this.stdout_('');
resolve(answer);
});
mutableStdout.muted = !!options.secure;
});
}

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
@@ -19,6 +19,7 @@ class Command extends BaseCommand {
let title = args['note'];
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
this.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', title));
const localFilePath = args['file'];

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
@@ -21,10 +21,6 @@ class Command extends BaseCommand {
];
}
enabled() {
return false;
}
async action(args) {
let title = args['note'];
@@ -33,6 +29,9 @@ class Command extends BaseCommand {
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
this.stdout(content);
app().gui().showConsole();
app().gui().maximizeConsole();
}
}

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { _, setLocale } = require('lib/locale.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
class Command extends BaseCommand {
@@ -23,7 +23,11 @@ class Command extends BaseCommand {
const verbose = args.options.verbose;
const renderKeyValue = (name) => {
const value = Setting.value(name);
const md = Setting.settingMetadata(name);
let value = Setting.value(name);
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
if (md.secure) value = '********';
if (Setting.isEnum(name)) {
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
} else {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
@@ -16,8 +16,9 @@ class Command extends BaseCommand {
return _('Marks a to-do as done.');
}
static async handleAction(args, isCompleted) {
static async handleAction(commandInstance, args, isCompleted) {
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
commandInstance.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', args.note));
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
@@ -32,7 +33,7 @@ class Command extends BaseCommand {
}
async action(args) {
await Command.handleAction(args, true);
await Command.handleAction(this, args, true);
}
}

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
class Command extends BaseCommand {

View File

@@ -0,0 +1,184 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem');
const Setting = require('lib/models/Setting.js');
class Command extends BaseCommand {
usage() {
return 'e2ee <command> [path]';
}
description() {
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.');
}
options() {
return [
// This is here mostly for testing - shouldn't be used
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
['-v, --verbose', 'More verbose output for the `target-status` command'],
];
}
async action(args) {
// change-password
const options = args.options;
if (args.command === 'enable') {
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
if (!password) {
this.stdout(_('Operation cancelled'));
return;
}
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
return;
}
if (args.command === 'disable') {
await EncryptionService.instance().disableEncryption();
return;
}
if (args.command === 'decrypt') {
this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.'));
while (true) {
try {
await DecryptionWorker.instance().start();
break;
} catch (error) {
if (error.code === 'masterKeyNotLoaded') {
const masterKeyId = error.masterKeyId;
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
if (!password) {
this.stdout(_('Operation cancelled'));
return;
}
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
await EncryptionService.instance().loadMasterKeysFromSettings();
continue;
}
throw error;
}
}
this.stdout(_('Completed decryption.'));
return;
}
if (args.command === 'status') {
this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled')));
return;
}
if (args.command === 'target-status') {
const fs = require('fs-extra');
const pathUtils = require('lib/path-utils.js');
const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)();
const targetPath = args.path;
if (!targetPath) throw new Error('Please specify the sync target path.');
const dirPaths = function(targetPath) {
let paths = [];
fs.readdirSync(targetPath).forEach((path) => {
paths.push(path);
});
return paths;
}
let itemCount = 0;
let resourceCount = 0;
let encryptedItemCount = 0;
let encryptedResourceCount = 0;
let otherItemCount = 0;
let encryptedPaths = [];
let decryptedPaths = [];
let paths = dirPaths(targetPath);
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const fullPath = targetPath + '/' + path;
const stat = await fs.stat(fullPath);
// this.stdout(fullPath);
if (path === '.resource') {
let resourcePaths = dirPaths(fullPath);
for (let j = 0; j < resourcePaths.length; j++) {
const resourcePath = resourcePaths[j];
resourceCount++;
const fullResourcePath = fullPath + '/' + resourcePath;
const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath);
if (isEncrypted) {
encryptedResourceCount++;
encryptedPaths.push(fullResourcePath);
} else {
decryptedPaths.push(fullResourcePath);
}
}
} else if (stat.isDirectory()) {
continue;
} else {
const content = await fs.readFile(fullPath, 'utf8');
const item = await BaseItem.unserialize(content);
const ItemClass = BaseItem.itemClass(item);
if (!ItemClass.encryptionSupported()) {
otherItemCount++;
continue;
}
itemCount++;
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
if (isEncrypted) {
encryptedItemCount++;
encryptedPaths.push(fullPath);
} else {
decryptedPaths.push(fullPath);
}
}
}
this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount);
this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount);
this.stdout('Other items (never encrypted): ' + otherItemCount);
if (options.verbose) {
this.stdout('');
this.stdout('# Encrypted paths');
this.stdout('');
for (let i = 0; i < encryptedPaths.length; i++) {
const path = encryptedPaths[i];
this.stdout(path);
}
this.stdout('');
this.stdout('# Decrypted paths');
this.stdout('');
for (let i = 0; i < decryptedPaths.length; i++) {
const path = decryptedPaths[i];
this.stdout(path);
}
}
return;
}
}
}
module.exports = Command;

View File

@@ -3,10 +3,10 @@ const { BaseCommand } = require('./base-command.js');
const { uuid } = require('lib/uuid.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Setting } = require('lib/models/setting.js');
const { BaseModel } = require('lib/base-model.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
const { time } = require('lib/time-utils.js');
@@ -44,6 +44,8 @@ class Command extends BaseCommand {
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
this.encryptionCheck(note);
if (!note) {
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
if (!ok) return;
@@ -76,12 +78,14 @@ class Command extends BaseCommand {
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
await app().gui().forceRender();
const termState = app().gui().term().saveState();
const termState = app().gui().termSaveState();
const spawnSync = require('child_process').spawnSync;
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
app().gui().term().restoreState(termState);
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
app().gui().termRestoreState(termState);
app().gui().hideModalOverlay();
app().gui().forceRender();

View File

@@ -12,6 +12,10 @@ class Command extends BaseCommand {
return _('Exits the application.');
}
compatibleUis() {
return ['gui'];
}
async action(args) {
await app().exit();
}

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { Exporter } = require('lib/services/exporter.js');
const { BaseModel } = require('lib/base-model.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {

View File

@@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { renderCommandHelp } = require('./help-utils.js');
const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { wrap } = require('lib/string-utils.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
@@ -18,7 +18,7 @@ class Command extends BaseCommand {
}
allCommands() {
const commands = app().commands();
const commands = app().commands(app().uiType());
let output = [];
for (let n in commands) {
if (!commands.hasOwnProperty(n)) continue;
@@ -36,21 +36,22 @@ class Command extends BaseCommand {
async action(args) {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts') {
if (args.command === 'shortcuts' || args.command === 'keymap') {
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'http://joplin.cozic.net/terminal/#shortcuts'));
this.stdout('');
if (app().gui().isDummy()) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const shortcuts = app().gui().shortcuts();
const keymap = app().gui().keymap();
let rows = [];
for (let n in shortcuts) {
if (!shortcuts.hasOwnProperty(n)) continue;
const shortcut = shortcuts[n];
if (!shortcut.description) continue;
n = shortcut.friendlyName ? shortcut.friendlyName : n;
rows.push([n, shortcut.description()]);
for (let i = 0; i < keymap.length; i++) {
const item = keymap[i];
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
rows.push([keys.join(', '), item.command]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
@@ -65,7 +66,7 @@ class Command extends BaseCommand {
} else {
const commandNames = this.allCommands().map((a) => a.name());
this.stdout(_('Type `help [command]` for more information about a command.'));
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
this.stdout('');
this.stdout(_('The possible commands are:'));
this.stdout('');
@@ -78,7 +79,7 @@ class Command extends BaseCommand {
this.stdout(_('To maximise/minimise the console, press "TC".'));
this.stdout(_('To enter command line mode, press ":"'));
this.stdout(_('To exit command line mode, press ESCAPE'));
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`'));
}
app().gui().showConsole();

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const Folder = require('lib/models/Folder.js');
const { importEnex } = require('lib/import-enex');
const { filename, basename } = require('lib/path-utils.js');
const { cliUtils } = require('./cli-utils.js');

View File

@@ -1,10 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Setting = require('lib/models/Setting.js');
const Note = require('lib/models/Note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { cliUtils } = require('./cli-utils.js');

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Folder } = require('lib/models/folder.js');
const Folder = require('lib/models/Folder.js');
const { reg } = require('lib/registry.js');
class Command extends BaseCommand {

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Note } = require('lib/models/note.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Note } = require('lib/models/note.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
@@ -20,6 +20,7 @@ class Command extends BaseCommand {
const name = args['name'];
const item = await app().loadItem('folderOrNote', pattern);
this.encryptionCheck(item);
if (!item) throw new Error(_('Cannot find "%s".', pattern));
const newItem = {

View File

@@ -1,10 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseModel } = require('lib/base-model.js');
const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook "%s"?', folder.title), { booleanAnswerDefault: 'n' });
const ok = force ? true : await this.prompt(_('Delete notebook? All notes within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
if (!ok) return;
await Folder.delete(folder.id);

View File

@@ -1,10 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseModel } = require('lib/base-model.js');
const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { uuid } = require('lib/uuid.js');

View File

@@ -1,10 +1,11 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { BaseItem } = require('lib/models/base-item.js');
const BaseModel = require('lib/BaseModel.js');
const { Database } = require('lib/database.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
class Command extends BaseCommand {
@@ -12,16 +13,16 @@ class Command extends BaseCommand {
return 'set <note> <name> [value]';
}
enabled() {
return false;
}
description() {
return _('Sets the property <name> of the given <note> to the given [value].');
}
const fields = Note.fields();
const s = [];
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
if (f.name === 'id') continue;
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
}
hidden() {
return true;
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
}
async action(args) {
@@ -34,6 +35,8 @@ class Command extends BaseCommand {
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
for (let i = 0; i < notes.length; i++) {
this.encryptionCheck(notes[i]);
let newNote = {
id: notes[i].id,
type_: notes[i].type_,

View File

@@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');

View File

@@ -2,8 +2,8 @@ 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 { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const Setting = require('lib/models/Setting.js');
const BaseItem = require('lib/models/BaseItem.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { reg } = require('lib/registry.js');
const { cliUtils } = require('./cli-utils.js');
@@ -11,6 +11,7 @@ const md5 = require('md5');
const locker = require('proper-lockfile');
const fs = require('fs-extra');
const osTmpdir = require('os-tmpdir');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class Command extends BaseCommand {
@@ -32,7 +33,6 @@ class Command extends BaseCommand {
options() {
return [
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
['--random-failures', 'For debugging purposes. Do not use.'],
];
}
@@ -62,14 +62,28 @@ class Command extends BaseCommand {
});
}
async doAuth(syncTargetId) {
async doAuth() {
const syncTarget = reg.syncTarget(this.syncTargetId_);
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
return auth;
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return false;
}
return true;
}
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
return false;
}
cancelAuth() {
@@ -87,7 +101,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir'));
const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
try {
@@ -121,12 +135,8 @@ class Command extends BaseCommand {
app().gui().showConsole();
app().gui().maximizeConsole();
const auth = await this.doAuth(this.syncTargetId_);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return cleanUp();
}
const authDone = await this.doAuth();
if (!authDone) return cleanUp();
}
const sync = await syncTarget.synchronizer();
@@ -140,7 +150,6 @@ class Command extends BaseCommand {
cliUtils.redrawDone();
this.stdout(msg);
},
randomFailures: args.options['random-failures'] === true,
};
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));

View File

@@ -1,8 +1,8 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { Tag } = require('lib/models/tag.js');
const { BaseModel } = require('lib/base-model.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
class Command extends BaseCommand {

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
@@ -25,6 +25,8 @@ class Command extends BaseCommand {
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
this.encryptionCheck(note);
let toSave = {
id: note.id,
};

View File

@@ -1,9 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
const CommandDone = require('./command-done.js');
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
}
async action(args) {
await CommandDone.handleAction(args, false);
await CommandDone.handleAction(this, args, false);
}
}

View File

@@ -1,8 +1,8 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
class Command extends BaseCommand {
@@ -18,8 +18,8 @@ class Command extends BaseCommand {
return { data: autocompleteFolders };
}
enabled() {
return false;
compatibleUis() {
return ['cli'];
}
async action(args) {

View File

@@ -1,5 +1,5 @@
const { BaseCommand } = require('./base-command.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {

View File

@@ -2,7 +2,7 @@
const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const { Resource } = require('lib/models/resource.js');
const Resource = require('lib/models/Resource.js');
const { dirname } = require('lib/path-utils.js');
const { FsDriverNode } = require('./fs-driver-node.js');
const lodash = require('lodash');

View File

@@ -1,6 +1,6 @@
const Folder = require('lib/models/folder.js').Folder;
const Tag = require('lib/models/tag.js').Tag;
const BaseModel = require('lib/base-model.js').BaseModel;
const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const ListWidget = require('tkwidgets/ListWidget.js');
const _ = require('lib/locale.js')._;
@@ -24,9 +24,9 @@ class FolderListWidget extends ListWidget {
if (item === '-') {
output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) {
output.push(item.title);
output.push(Folder.displayTitle(item));
} else if (item.type_ === Tag.modelType()) {
output.push('[' + item.title + ']');
output.push('[' + Folder.displayTitle(item) + ']');
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:'));
output.push(item.title);

View File

@@ -1,4 +1,4 @@
const Note = require('lib/models/note.js').Note;
const Note = require('lib/models/Note.js');
const ListWidget = require('tkwidgets/ListWidget.js');
class NoteListWidget extends ListWidget {
@@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget {
this.updateIndexFromSelectedNoteId_ = false;
this.itemRenderer = (note) => {
let label = note.title; // + ' ' + note.id;
let label = Note.displayTitle(note); // + ' ' + note.id;
if (note.is_todo) {
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
}

View File

@@ -1,4 +1,4 @@
const Note = require('lib/models/note.js').Note;
const Note = require('lib/models/Note.js');
const TextWidget = require('tkwidgets/TextWidget.js');
class NoteMetadataWidget extends TextWidget {

View File

@@ -1,5 +1,6 @@
const Note = require('lib/models/note.js').Note;
const Note = require('lib/models/Note.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const { _ } = require('lib/locale.js');
class NoteWidget extends TextWidget {
@@ -32,11 +33,24 @@ class NoteWidget extends TextWidget {
this.reloadNote();
}
welcomeText() {
return _('Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.');
}
reloadNote() {
if (this.noteId_) {
if (!this.noteId_ && !this.notes.length) {
this.text = this.welcomeText();
this.scrollTop = 0;
} else if (this.noteId_) {
this.doAsync('loadNote', async () => {
this.note_ = await Note.load(this.noteId_);
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
if (this.note_ && this.note_.encryption_applied) {
this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.');
} else {
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
}
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
this.lastLoadedNoteId_ = this.noteId_;
});

View File

@@ -2,6 +2,7 @@ const BaseWidget = require('tkwidgets/BaseWidget.js');
const chalk = require('chalk');
const termutils = require('tkwidgets/framework/termutils.js');
const stripAnsi = require('strip-ansi');
const { handleAutocompletion } = require('../autocompletion.js');
class StatusBarWidget extends BaseWidget {
@@ -41,6 +42,7 @@ class StatusBarWidget extends BaseWidget {
};
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
if ('secure' in options) this.promptState_.secure = options.secure;
this.promptState_.promise = new Promise((resolve, reject) => {
this.promptState_.resolve = resolve;
@@ -104,13 +106,19 @@ class StatusBarWidget extends BaseWidget {
this.term.showCursor(true);
const isSecurePrompt = !!this.promptState_.secure;
let options = {
cancelable: true,
history: this.history,
default: this.promptState_.initialText,
autoComplete: handleAutocompletion,
autoCompleteHint : true,
autoCompleteMenu : true,
};
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
if (isSecurePrompt) options.echoChar = true;
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
let resolveResult = null;
@@ -125,7 +133,7 @@ class StatusBarWidget extends BaseWidget {
resolveResult = input ? input.trim() : input;
// Add the command to history but only if it's longer than one character.
// Below that it's usually an answer like "y"/"n", etc.
if (input && input.length > 1) this.history_.push(input);
if (!isSecurePrompt && input && input.length > 1) this.history_.push(input);
}
}
@@ -159,4 +167,4 @@ class StatusBarWidget extends BaseWidget {
}
module.exports = StatusBarWidget;
module.exports = StatusBarWidget;

View File

@@ -1,6 +1,6 @@
const fs = require('fs-extra');
const { wrap } = require('lib/string-utils.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const { _, setLocale, languageCode } = require('lib/locale.js');

View File

@@ -1,26 +1,36 @@
#!/usr/bin/env node
// Loading time: 20170803: 1.5s with no commands
// Make it possible to require("/lib/...") without specifying full path
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);
process.exit(1);
}
const { app } = require('./app.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Resource } = require('lib/models/resource.js');
const { BaseItem } = require('lib/models/base-item.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { NoteTag } = require('lib/models/note-tag.js');
const { Setting } = require('lib/models/setting.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');
const BaseItem = require('lib/models/BaseItem.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting.js');
const { Logger } = require('lib/logger.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
const { _ } = require('lib/locale.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const EncryptionService = require('lib/services/EncryptionService');
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
@@ -29,6 +39,7 @@ BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');
@@ -55,6 +66,53 @@ process.stdout.on('error', function( err ) {
}
});
// async function main() {
// const WebDavApi = require('lib/WebDavApi');
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
// const driver = new FileApiDriverWebDav(api);
// const stat = await driver.stat('');
// console.info(stat);
// // const stat = await driver.stat('testing.txt');
// // console.info(stat);
// // const content = await driver.get('testing.txta');
// // console.info(content);
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
// // console.info(content);
// // const content = await driver.mkdir('newdir5');
// // console.info(content);
// //await driver.put('myfile4.md', 'this is my content');
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
// // await driver.delete('myfile4.md');
// // const deltaResult = await driver.delta('', {
// // allItemIdsHandler: () => { return []; }
// // });
// // console.info(deltaResult);
// }
// main().catch((error) => { console.error(error); });
application.start(process.argv).catch((error) => {
console.error(_('Fatal error:'));
console.error(error);

View File

@@ -1,3 +0,0 @@
#/bin/bash
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
NODE_PATH="$CLIENT_DIR/build" node "$CLIENT_DIR/build/build-translation.js" --silent

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -e
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
"$ROOT_DIR/build.sh" && NODE_PATH="$ROOT_DIR/build" node "$ROOT_DIR/build/build-website.js"

View File

@@ -4,9 +4,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_DIR="$ROOT_DIR/build"
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
chmod 755 "$BUILD_DIR/main.js"
# cd "$BUILD_DIR"
# node build-translation.js --silent
chmod 755 "$BUILD_DIR/main.js"

View File

@@ -1,6 +0,0 @@
#!/bin/bash
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
"$CLIENT_DIR/publish.sh"
npm install -g joplin

1430
CliClient/locales/de_DE.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,63 +15,12 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Give focus to next pane"
msgstr ""
msgid "Give focus to previous pane"
msgstr ""
msgid "Enter command line mode"
msgstr ""
msgid "Exit command line mode"
msgstr ""
msgid "Edit the selected note"
msgstr ""
msgid "Cancel the current command."
msgstr ""
msgid "Exit the application."
msgstr ""
msgid "Delete the currently selected note or notebook."
msgstr ""
msgid "To delete a tag, untag the associated notes."
msgstr ""
msgid "Please select the note or notebook to be deleted first."
msgstr ""
msgid "Set a to-do as completed / not completed"
msgstr ""
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
msgid "Search"
msgstr ""
msgid "[t]oggle note [m]etadata."
msgstr ""
msgid "[M]ake a new [n]ote"
msgstr ""
msgid "[M]ake a new [t]odo"
msgstr ""
msgid "[M]ake a new note[b]ook"
msgstr ""
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr ""
msgid "Move the note to a notebook."
msgstr ""
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr ""
@@ -100,10 +49,17 @@ msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
#, javascript-format
msgid "No such command: %s"
msgstr ""
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr ""
@@ -161,6 +117,35 @@ msgstr ""
msgid "Note is not a to-do: \"%s\""
msgstr ""
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
msgid "Enter master password:"
msgstr ""
msgid "Operation cancelled"
msgstr ""
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
msgid "Completed decryption."
msgstr ""
msgid "Enabled"
msgstr ""
msgid "Disabled"
msgstr ""
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr ""
@@ -178,6 +163,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -201,10 +190,16 @@ msgstr ""
msgid "Displays usage information."
msgstr ""
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr ""
msgid "Type `help [command]` for more information about a command."
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
msgid "The possible commands are:"
@@ -234,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
msgstr ""
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
msgid "Imports an Evernote notebook file (.enex file)."
@@ -339,8 +334,7 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation."
msgstr ""
#, javascript-format
msgid "Delete notebook \"%s\"?"
msgid "Delete notebook? All notes within this notebook will also be deleted."
msgstr ""
msgid "Deletes the notes matching <note-pattern>."
@@ -359,7 +353,12 @@ msgstr ""
msgid "Searches for the given <pattern> in all the notes."
msgstr ""
msgid "Sets the property <name> of the given <note> to the given [value]."
#, javascript-format
msgid ""
"Sets the property <name> of the given <note> to the given [value]. Possible "
"properties are:\n"
"\n"
"%s"
msgstr ""
msgid "Displays summary about the notes and notebooks."
@@ -371,6 +370,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -381,10 +388,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -469,6 +472,22 @@ msgstr ""
msgid "Search:"
msgstr ""
msgid ""
"Welcome to Joplin!\n"
"\n"
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
"for usage information.\n"
"\n"
"For example, to create a notebook press `mb`; to create a note press `mn`."
msgstr ""
msgid ""
"One or more items are currently encrypted and you may need to supply a "
"master password. To do so please type `e2ee decrypt`. If you have already "
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
msgid "File"
msgstr ""
@@ -487,6 +506,10 @@ msgstr ""
msgid "Evernote Export Files"
msgstr ""
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr ""
@@ -505,10 +528,22 @@ msgstr ""
msgid "Search in all the notes"
msgstr ""
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr ""
msgid "Options"
msgid "Synchronisation status"
msgstr ""
msgid "Encryption options"
msgstr ""
msgid "General Options"
msgstr ""
msgid "Help"
@@ -517,6 +552,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -524,12 +562,112 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent encrypted to the sync target. Do not lose the "
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
msgid "Disable encryption"
msgstr ""
msgid "Enable encryption"
msgstr ""
msgid "Master Keys"
msgstr ""
msgid "Active"
msgstr ""
msgid "ID"
msgstr ""
msgid "Source"
msgstr ""
msgid "Created"
msgstr ""
msgid "Updated"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password OK"
msgstr ""
msgid ""
"Note: Only one master key is going to be used for encryption (the one marked "
"as \"active\"). Any of the keys might be used for decryption, depending on "
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
msgid "Encryption is:"
msgstr ""
msgid "Back"
msgstr ""
@@ -541,15 +679,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr ""
msgid "Note title:"
msgstr ""
msgid "Please create a notebook first"
msgstr ""
msgid "To-do title:"
msgstr ""
msgid "Notebook title:"
msgstr ""
@@ -565,9 +697,24 @@ msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Search"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Some items cannot be synchronised."
msgstr ""
msgid "View them now"
msgstr ""
msgid "Some items cannot be decrypted."
msgstr ""
msgid "Set the password"
msgstr ""
msgid "Add or remove tags"
msgstr ""
@@ -583,6 +730,16 @@ msgstr ""
msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
msgid "Open..."
msgstr ""
msgid "Save as..."
msgstr ""
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr ""
@@ -590,9 +747,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "to-do"
msgstr ""
msgid "note"
msgstr ""
#, javascript-format
msgid "Creating new %s..."
msgstr ""
msgid "Refresh"
msgstr ""
@@ -605,7 +775,13 @@ msgstr ""
msgid "Import"
msgstr ""
msgid "Delete notebook?"
msgid "Options"
msgstr ""
msgid "Synchronisation Status"
msgstr ""
msgid "Encryption Options"
msgstr ""
msgid "Remove this tag from all the notes?"
@@ -623,10 +799,10 @@ msgstr ""
msgid "Notebooks"
msgstr ""
msgid "Tags"
msgid "Searches"
msgstr ""
msgid "Searches"
msgid "Please select where the sync status should be exported to"
msgstr ""
#, javascript-format
@@ -640,12 +816,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -696,6 +878,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@@ -711,6 +897,12 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid "Encrypted"
msgstr ""
msgid "Encrypted items cannot be modified"
msgstr ""
msgid "Conflicts"
msgstr ""
@@ -736,14 +928,6 @@ msgstr ""
msgid "Cannot move note to \"%s\" notebook"
msgstr ""
msgid "File system synchronisation target directory"
msgstr ""
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
msgid "Text editor"
msgstr ""
@@ -770,16 +954,34 @@ msgstr ""
msgid "Dark"
msgstr ""
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Synchronisation interval"
msgid "When creating a new to-do:"
msgstr ""
msgid "Disabled"
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
#, javascript-format
@@ -794,9 +996,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -804,14 +1003,53 @@ msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
msgid "Nextcloud WebDAV URL"
msgstr ""
msgid "Nextcloud username"
msgstr ""
msgid "Nextcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "%s (%s): %s"
msgstr ""
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
msgid "Sync status (synced items / total items)"
msgstr ""
@@ -854,10 +1092,10 @@ msgstr ""
msgid "Log"
msgstr ""
msgid "Status"
msgid "Export Debug Report"
msgstr ""
msgid "Export Debug Report"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
@@ -870,6 +1108,9 @@ msgstr ""
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr ""
@@ -879,6 +1120,23 @@ msgstr ""
msgid "Cancel synchronisation"
msgstr ""
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, javascript-format
msgid "Created: %s"
msgstr ""
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
msgid "Enable"
msgstr ""
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr ""
@@ -886,6 +1144,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

File diff suppressed because it is too large Load Diff

1397
CliClient/locales/es_ES.po Normal file

File diff suppressed because it is too large Load Diff

1372
CliClient/locales/eu.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Last-Translator: Laurent Cozic\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
@@ -15,63 +15,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.3\n"
msgid "Give focus to next pane"
msgstr "Activer le volet suivant"
msgid "Give focus to previous pane"
msgstr "Activer le volet précédent"
msgid "Enter command line mode"
msgstr "Démarrer le mode de ligne de commande"
msgid "Exit command line mode"
msgstr "Sortir du mode de ligne de commande"
msgid "Edit the selected note"
msgstr "Éditer la note sélectionnée"
msgid "Cancel the current command."
msgstr "Annuler la commande en cours."
msgid "Exit the application."
msgstr "Quitter le logiciel."
msgid "Delete the currently selected note or notebook."
msgstr "Supprimer la note ou carnet sélectionné."
msgid "To delete a tag, untag the associated notes."
msgstr "Pour supprimer une vignette, enlever là des notes associées."
msgid "Please select the note or notebook to be deleted first."
msgstr "Veuillez d'abord sélectionner un carnet."
msgid "Set a to-do as completed / not completed"
msgstr "Marquer une tâches comme complétée / non-complétée"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
msgid "Search"
msgstr "Chercher"
msgid "[t]oggle note [m]etadata."
msgstr "Afficher/Cacher les métadonnées des notes."
msgid "[M]ake a new [n]ote"
msgstr "Créer une nouvelle note"
msgid "[M]ake a new [t]odo"
msgstr "Créer une nouvelle tâche"
msgid "[M]ake a new note[b]ook"
msgstr "Créer un nouveau carnet"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copier la note dans un autre carnet."
msgid "Move the note to a notebook."
msgstr "Déplacer la note vers un carnet."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Appuyez sur Ctrl+D ou tapez \"exit\" pour sortir du logiciel"
@@ -100,11 +49,18 @@ msgstr "o"
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annulation de la synchronisation... Veuillez patienter."
#, javascript-format
msgid "No such command: %s"
msgstr "Commande invalide : %s"
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
"La commande \"%s\" est disponible uniquement en mode d'interface graphique"
msgid "Cannot change encrypted item"
msgstr "Un objet crypté ne peut pas être modifié"
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Paramètre requis manquant : %s"
@@ -167,6 +123,39 @@ msgstr "Marquer la tâche comme complétée."
msgid "Note is not a to-do: \"%s\""
msgstr "La note n'est pas une tâche : \"%s\""
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Gérer la configuration E2EE (Cryptage de bout à bout). Les commandes sont "
"`enable`, `disable`, `decrypt` et `status` et `target-status`."
msgid "Enter master password:"
msgstr "Entrer le mot de passe maître :"
msgid "Operation cancelled"
msgstr "Opération annulée"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Démarrage du décryptage... Veuillez patienter car cela pourrait prendre "
"plusieurs minutes selon le nombre d'objets à décrypter."
msgid "Completed decryption."
msgstr "Décryptage complété."
msgid "Enabled"
msgstr "Activé"
msgid "Disabled"
msgstr "Désactivé"
#, javascript-format
msgid "Encryption is: %s"
msgstr "Le cryptage est : %s"
msgid "Edit note."
msgstr "Éditer la note."
@@ -188,6 +177,10 @@ msgstr ""
"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à "
"l'invite de commande."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Erreur lors de l'ouverture de la note dans l'éditeur de texte : %s"
msgid "Note has been saved."
msgstr "La note a été enregistrée."
@@ -214,11 +207,19 @@ msgstr "Afficher l'URL de l'emplacement de la note."
msgid "Displays usage information."
msgstr "Affiche les informations d'utilisation."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Les raccourcis ne sont pas disponible en mode de ligne de commande."
msgid "Type `help [command]` for more information about a command."
msgstr "Tapez `help [command]` pour plus d'information sur une commande."
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
"Tapez `help [command]` pour plus d'information sur une commande ; ou tapez "
"`help all` pour l'aide complète."
msgid "The possible commands are:"
msgstr "Les commandes possibles sont :"
@@ -252,8 +253,9 @@ msgstr "Pour démarrer le mode ligne de commande, pressez \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Pour sortir du mode ligne de commande, pressez ECHAP"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Pour la liste complète des raccourcis disponibles, tapez `help shortcuts`"
@@ -370,9 +372,10 @@ msgstr "Supprimer le carnet."
msgid "Deletes the notebook without asking for confirmation."
msgstr "Supprimer le carnet sans demander la confirmation."
#, javascript-format
msgid "Delete notebook \"%s\"?"
msgstr "Supprimer le carnet \"%s\" ?"
msgid "Delete notebook? All notes within this notebook will also be deleted."
msgstr ""
"Effacer le carnet ? Toutes les notes dans ce carnet seront également "
"effacées."
msgid "Deletes the notes matching <note-pattern>."
msgstr "Supprimer les notes correspondants à <note-pattern>."
@@ -390,8 +393,17 @@ msgstr "Supprimer la note ?"
msgid "Searches for the given <pattern> in all the notes."
msgstr "Chercher le motif <pattern> dans toutes les notes."
msgid "Sets the property <name> of the given <note> to the given [value]."
msgstr "Assigner la valeur [value] à la propriété <name> de la <note> donnée."
#, javascript-format
msgid ""
"Sets the property <name> of the given <note> to the given [value]. Possible "
"properties are:\n"
"\n"
"%s"
msgstr ""
"Assigner la valeur [value] à la propriété <name> de la <note> donnée. Les "
"valeurs possibles sont :\n"
"\n"
"%s"
msgid "Displays summary about the notes and notebooks."
msgstr "Afficher un résumé des notes et carnets."
@@ -404,6 +416,16 @@ msgstr ""
"Synchroniser avec la cible donnée (par défaut, la valeur de configuration "
"`sync.target`)."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Non-connecté à %s. Veuillez fournir les identifiants et mots de passe "
"manquants."
msgid "Synchronisation is already in progress."
msgstr "La synchronisation est déjà en cours."
@@ -417,10 +439,6 @@ msgstr ""
"correctement. Si vous savez qu'aucune autre synchronisation est en cours, "
"vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Cible de la synchronisation : %s (%s)"
@@ -519,6 +537,33 @@ msgstr ""
msgid "Search:"
msgstr "Recherche :"
msgid ""
"Welcome to Joplin!\n"
"\n"
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
"for usage information.\n"
"\n"
"For example, to create a notebook press `mb`; to create a note press `mn`."
msgstr ""
"Bienvenue dans Joplin!\n"
"\n"
"Tapez `:help shortcuts` pour la liste des raccourcis claviers, ou simplement "
"`:help` pour une vue d'ensemble.\n"
"\n"
"Par exemple, pour créer un carnet, pressez `mb` ; pour créer une note "
"pressed `mn`."
msgid ""
"One or more items are currently encrypted and you may need to supply a "
"master password. To do so please type `e2ee decrypt`. If you have already "
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Au moins un objet est actuellement crypté et il se peut que vous deviez "
"fournir votre mot de passe maître. Pour se faire, veuillez taper `e2ee "
"decrypt`. Si vous avez déjà fourni ce mot de passe, les objets cryptés vont "
"être décrypté en tâche de fond et seront disponible prochainement."
msgid "File"
msgstr "Fichier"
@@ -537,6 +582,10 @@ msgstr "Importer notes d'Evernote"
msgid "Evernote Export Files"
msgstr "Fichiers d'export Evernote"
#, javascript-format
msgid "Hide %s"
msgstr "Cacher %s"
msgid "Quit"
msgstr "Quitter"
@@ -555,11 +604,23 @@ msgstr "Coller"
msgid "Search in all the notes"
msgstr "Chercher dans toutes les notes"
msgid "View"
msgstr "Affichage"
msgid "Toggle editor layout"
msgstr "Basculer l'agencement de l'éditeur"
msgid "Tools"
msgstr "Outils"
msgid "Options"
msgstr "Options"
msgid "Synchronisation status"
msgstr "État de la synchronisation"
msgid "Encryption options"
msgstr "Options de cryptage"
msgid "General Options"
msgstr "Options générales"
msgid "Help"
msgstr "Aide"
@@ -567,6 +628,9 @@ msgstr "Aide"
msgid "Website and documentation"
msgstr "Documentation en ligne"
msgid "Check for updates..."
msgstr "Vérifier les mises à jour..."
msgid "About Joplin"
msgstr "A propos de Joplin"
@@ -574,11 +638,130 @@ msgstr "A propos de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, javascript-format
msgid "Open %s"
msgstr "Ouvrir %s"
msgid "Exit"
msgstr "Quitter"
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annulation"
msgstr "Annuler"
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
"Notes de version :\n"
"\n"
"%s"
msgid "An update is available, do you want to download it now?"
msgstr ""
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
msgid "Check synchronisation configuration"
msgstr "Vérifier config synchronisation"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Les notes et paramètres se trouve dans : %s"
msgid "Save"
msgstr "Enregistrer"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Désactiver le cryptage signifie que *toutes* les notes et fichiers vont être "
"re-synchronisés et envoyés décryptés sur la cible de la synchronisation. "
"Souhaitez vous continuer ?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent encrypted to the sync target. Do not lose the "
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
"Activer le cryptage signifie que *toutes* les notes et fichiers vont être re-"
"synchronisés et envoyés cryptés vers la cible de la synchronisation. Ne "
"perdez pas votre mot de passe car, pour des raisons de sécurité, ce sera la "
"*seule* façon de décrypter les données ! Pour activer le cryptage, veuillez "
"entrer votre mot de passe ci-dessous."
msgid "Disable encryption"
msgstr "Désactiver le cryptage"
msgid "Enable encryption"
msgstr "Activer le cryptage"
msgid "Master Keys"
msgstr "Clefs maître"
msgid "Active"
msgstr "Actif"
msgid "ID"
msgstr "ID"
msgid "Source"
msgstr "Source"
msgid "Created"
msgstr "Créé"
msgid "Updated"
msgstr "Mis à jour"
msgid "Password"
msgstr "Mot de passe"
msgid "Password OK"
msgstr "Mot de passe OK"
msgid ""
"Note: Only one master key is going to be used for encryption (the one marked "
"as \"active\"). Any of the keys might be used for decryption, depending on "
"how the notes or notebooks were originally encrypted."
msgstr ""
"Note : seule une clef maître va être utilisée pour le cryptage (celle "
"marquée comme \"actif\" ci-dessus). N'importe quel clef peut-être utilisée "
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
"à l'origine."
msgid "Missing Master Keys"
msgstr "Clefs maître manquantes"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
"probable qu'elle vont être prochainement disponible via la synchronisation."
msgid "Status"
msgstr "État"
msgid "Encryption is:"
msgstr "Le cryptage est :"
msgid "Back"
msgstr "Retour"
@@ -593,15 +776,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Veuillez d'abord sélectionner un carnet."
msgid "Note title:"
msgstr "Titre de la note :"
msgid "Please create a notebook first"
msgstr "Veuillez d'abord créer un carnet d'abord"
msgid "To-do title:"
msgstr "Titre de la tâche :"
msgid "Notebook title:"
msgstr "Titre du carnet :"
@@ -614,13 +791,27 @@ msgstr "Séparez chaque étiquette par une virgule."
msgid "Rename notebook:"
msgstr "Renommer le carnet :"
#, fuzzy
msgid "Set alarm:"
msgstr "Définir ou modifier alarme"
msgstr "Régler alarme :"
msgid "Search"
msgstr "Chercher"
msgid "Layout"
msgstr "Disposition"
msgid "Some items cannot be synchronised."
msgstr "Certains objets ne peuvent être synchronisés."
msgid "View them now"
msgstr "Les voir maintenant"
msgid "Some items cannot be decrypted."
msgstr "Certains objets ne peuvent être décryptés."
msgid "Set the password"
msgstr "Définir le mot de passe"
msgid "Add or remove tags"
msgstr "Gérer les étiquettes"
@@ -637,6 +828,18 @@ msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\"."
msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
"carnet\"."
msgid "Open..."
msgstr "Ouvrir..."
msgid "Save as..."
msgstr "Enregistrer sous..."
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Lien ou message non géré : %s"
@@ -644,9 +847,21 @@ msgstr "Lien ou message non géré : %s"
msgid "Attach file"
msgstr "Attacher un fichier"
#, fuzzy
msgid "Tags"
msgstr "Étiquettes"
msgid "Set alarm"
msgstr "Définir ou modifier alarme"
msgstr "Régler alarme"
msgid "to-do"
msgstr "tâche"
msgid "note"
msgstr "note"
#, javascript-format
msgid "Creating new %s..."
msgstr "Création de %s..."
msgid "Refresh"
msgstr "Rafraîchir"
@@ -660,8 +875,14 @@ msgstr "Connexion OneDrive"
msgid "Import"
msgstr "Importer"
msgid "Delete notebook?"
msgstr "Supprimer le carnet ?"
msgid "Options"
msgstr "Options"
msgid "Synchronisation Status"
msgstr "État de la synchronisation"
msgid "Encryption Options"
msgstr "Options de cryptage"
msgid "Remove this tag from all the notes?"
msgstr "Enlever cette étiquette de toutes les notes ?"
@@ -678,12 +899,13 @@ msgstr "Synchroniser"
msgid "Notebooks"
msgstr "Carnets"
msgid "Tags"
msgstr "Étiquettes"
msgid "Searches"
msgstr "Recherches"
msgid "Please select where the sync status should be exported to"
msgstr ""
"Veuillez sélectionner un répertoire ou exporter l'état de la synchronisation"
#, javascript-format
msgid "Usage: %s"
msgstr "Utilisation : %s"
@@ -695,12 +917,18 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud"
msgstr "Nextcloud"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
msgid "WebDAV"
msgstr "WebDAV"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@@ -724,6 +952,12 @@ msgid ""
"\n"
"Please consider using a regular OneDrive account."
msgstr ""
"Impossible de synchroniser avec OneDrive.\n"
"\n"
"Cette erreur se produit lors de l'utilisation de OneDrive for Business, qui "
"malheureusement n'est pas compatible.\n"
"\n"
"Veuillez utiliser à la place un compte OneDrive normal."
#, javascript-format
msgid "Cannot access %s"
@@ -753,6 +987,10 @@ msgstr "Objets supprimés localement : %d."
msgid "Deleted remote items: %d."
msgstr "Objets distants supprimés : %d."
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Téléchargés : %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "État : \"%s\"."
@@ -768,6 +1006,12 @@ msgstr "Terminé : %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La synchronisation est déjà en cours. État : %s"
msgid "Encrypted"
msgstr "Crypté"
msgid "Encrypted items cannot be modified"
msgstr "Les objets cryptés ne peuvent être modifiés"
msgid "Conflicts"
msgstr "Conflits"
@@ -793,16 +1037,6 @@ msgstr "Impossible de copier la note vers le carnet \"%s\""
msgid "Cannot move note to \"%s\" notebook"
msgstr "Impossible de déplacer la note vers le carnet \"%s\""
msgid "File system synchronisation target directory"
msgstr "Cible de la synchronisation sur le disque dur"
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Text editor"
msgstr "Éditeur de texte"
@@ -831,18 +1065,36 @@ msgstr "Clair"
msgid "Dark"
msgstr "Sombre"
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
msgid "When creating a new to-do:"
msgstr "Lors de la création d'une tâche :"
msgid "Focus title"
msgstr "Curseur sur le titre"
msgid "Focus body"
msgstr "Curseur sur corps du message"
msgid "When creating a new note:"
msgstr "Lors de la création d'une note :"
msgid "Show tray icon"
msgstr "Afficher icône dans la zone de notifications"
msgid "Set application zoom percentage"
msgstr "Niveau de zoom"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Synchronisation interval"
msgstr "Intervalle de synchronisation"
msgid "Disabled"
msgstr "Désactivé"
#, javascript-format
msgid "%d minutes"
msgstr "%d minutes"
@@ -855,9 +1107,6 @@ msgstr "%d heure"
msgid "%d hours"
msgstr "%d heures"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Show advanced options"
msgstr "Montrer les options avancées"
@@ -865,16 +1114,61 @@ msgid "Synchronisation target"
msgstr "Cible de la synchronisation"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
"La cible avec laquelle synchroniser. Chaque cible de synchronisation peut "
"avoir des paramètres supplémentaires sous le nom `sync.NUM.NOM` (documentés "
"ci-dessous)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Nextcloud WebDAV URL"
msgstr "Nextcloud : URL WebDAV"
msgid "Nextcloud username"
msgstr "Nextcloud : Nom utilisateur"
msgid "Nextcloud password"
msgstr "Nextcloud : Mot de passe"
msgid "WebDAV URL"
msgstr "WebDAV : URL"
msgid "WebDAV username"
msgstr "WebDAV : Nom utilisateur"
msgid "WebDAV password"
msgstr "WebDAV : Mot de passe"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
msgid "Items that cannot be synchronised"
msgstr "Objets qui ne peuvent pas être synchronisés"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s) : %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
"Ces objets resteront sur l'appareil mais ne seront pas envoyé sur la cible "
"de la synchronisation. Pour trouver ces objets, faite une recherche sur le "
"titre ou l'identifiant de l'objet (affiché ci-dessus entre parenthèses)."
msgid "Sync status (synced items / total items)"
msgstr "Status de la synchronisation (objets synchro. / total)"
@@ -919,22 +1213,25 @@ msgstr "Supprimer ces notes ?"
msgid "Log"
msgstr "Journal"
msgid "Status"
msgstr "État"
msgid "Export Debug Report"
msgstr "Exporter rapport de débogage"
msgid "Encryption Config"
msgstr "Config cryptage"
msgid "Configuration"
msgstr "Configuration"
msgid "Move to notebook..."
msgstr "Déplacer la note vers carnet..."
msgstr "Déplacer vers..."
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Déplacer %d notes vers carnet \"%s\" ?"
msgid "Press to set the decryption password."
msgstr "Définir mot de passe de synchronisation."
msgid "Select date"
msgstr "Sélectionner date"
@@ -944,6 +1241,23 @@ msgstr "Confirmer"
msgid "Cancel synchronisation"
msgstr "Annuler synchronisation"
#, javascript-format
msgid "Master Key %s"
msgstr "Clef maître %s"
#, javascript-format
msgid "Created: %s"
msgstr "Créé : %s"
msgid "Password:"
msgstr "Mot de passe :"
msgid "Password cannot be empty"
msgstr "Mot de passe ne peut être vide"
msgid "Enable"
msgstr "Activer"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
@@ -951,6 +1265,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
msgid "Edit notebook"
msgstr "Éditer le carnet"
msgid "Show all"
msgstr "Afficher tous"
msgid "Errors only"
msgstr "Erreurs seulement"
msgid "This note has been modified:"
msgstr "Cette note a été modifiée :"
@@ -980,10 +1300,10 @@ msgid "Hide metadata"
msgstr "Cacher les métadonnées"
msgid "Show metadata"
msgstr "Afficher les métadonnées"
msgstr "Voir métadonnées"
msgid "View on map"
msgstr "Voir emplacement sur carte"
msgstr "Voir sur carte"
msgid "Delete notebook"
msgstr "Supprimer le carnet"
@@ -1006,6 +1326,90 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ msgid "Give focus to next pane"
#~ msgstr "Activer le volet suivant"
#~ msgid "Give focus to previous pane"
#~ msgstr "Activer le volet précédent"
#~ msgid "Enter command line mode"
#~ msgstr "Démarrer le mode de ligne de commande"
#~ msgid "Exit command line mode"
#~ msgstr "Sortir du mode de ligne de commande"
#~ msgid "Edit the selected note"
#~ msgstr "Éditer la note sélectionnée"
#~ msgid "Cancel the current command."
#~ msgstr "Annuler la commande en cours."
#~ msgid "Exit the application."
#~ msgstr "Quitter le logiciel."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Supprimer la note ou carnet sélectionné."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Marquer une tâches comme complétée / non-complétée"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "Afficher/Cacher les métadonnées des notes."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "Créer une nouvelle note"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "Créer une nouvelle tâche"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "Créer un nouveau carnet"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copier la note dans un autre carnet."
#~ msgid "Move the note to a notebook."
#~ msgstr "Déplacer la note vers un carnet."
#~ msgid "Error"
#~ msgstr "Erreur"
#~ msgid "WebDAV (Beta)"
#~ msgstr "WebDAV (Bêta)"
#~ msgid "Could not download the update: %s"
#~ msgstr "Impossible de télécharger la mise à jour : %s"
#~ msgid "New version downloaded - application will quit now and update..."
#~ msgstr ""
#~ "La nouvelle version a été téléchargée - le programme va se fermer et se "
#~ "mettre à jour..."
#~ msgid "Could not install the update: %s"
#~ msgstr "Impossible d'installer la mise à jour : %s"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
#~ "fichier, veuillez spécifier le répertoire avec `sync.2.path`."
#~ msgid "To-do title:"
#~ msgstr "Titre de la tâche :"
#~ msgid "Delete notebook?"
#~ msgstr "Supprimer le carnet ?"
#~ msgid "Delete notebook \"%s\"?"
#~ msgstr "Supprimer le carnet \"%s\" ?"
#~ msgid "File system synchronisation target directory"
#~ msgstr "Cible de la synchronisation sur le disque dur"
#~ msgid "Set or clear alarm:"
#~ msgstr "Définir ou modifier alarme :"
@@ -1058,9 +1462,6 @@ msgstr "Bienvenue"
#~ msgid "Todo filter"
#~ msgstr "Filtre des tâches"
#~ msgid "Show all"
#~ msgstr "Afficher tous"
#~ msgid "Non-completed and recently completed ones"
#~ msgstr "Tâches non-complétées et récentes"
@@ -1071,17 +1472,10 @@ msgstr "Bienvenue"
#~ msgid "Delete a note"
#~ msgstr "Supprimer la note"
#~ msgid "%s (%s)"
#~ msgstr "%s (%s)"
#, fuzzy
#~ msgid "Show/Hide the console"
#~ msgstr "Quitter le logiciel."
#, fuzzy
#~ msgid "Last command: %s"
#~ msgstr "Commande invalide : \"%s\""
#~ msgid "Done editing."
#~ msgstr "Edition terminée."
@@ -1183,12 +1577,6 @@ msgstr "Bienvenue"
#~ "Tous les ports sont en cours d'utilisation. Veuillez signaler ce problème "
#~ "sur %s"
#~ msgid ""
#~ "There is currently no notebook. Create one by clicking on the (+) button."
#~ msgstr ""
#~ "Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur le "
#~ "bouton (+)"
#~ msgid "Synchronizing with directory \"%s\""
#~ msgstr "Synchronisation avec dossier \"%s\""

1370
CliClient/locales/hr_HR.po Normal file

File diff suppressed because it is too large Load Diff

1364
CliClient/locales/it_IT.po Normal file

File diff suppressed because it is too large Load Diff

1353
CliClient/locales/ja_JP.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,63 +15,12 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Give focus to next pane"
msgstr ""
msgid "Give focus to previous pane"
msgstr ""
msgid "Enter command line mode"
msgstr ""
msgid "Exit command line mode"
msgstr ""
msgid "Edit the selected note"
msgstr ""
msgid "Cancel the current command."
msgstr ""
msgid "Exit the application."
msgstr ""
msgid "Delete the currently selected note or notebook."
msgstr ""
msgid "To delete a tag, untag the associated notes."
msgstr ""
msgid "Please select the note or notebook to be deleted first."
msgstr ""
msgid "Set a to-do as completed / not completed"
msgstr ""
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
msgid "Search"
msgstr ""
msgid "[t]oggle note [m]etadata."
msgstr ""
msgid "[M]ake a new [n]ote"
msgstr ""
msgid "[M]ake a new [t]odo"
msgstr ""
msgid "[M]ake a new note[b]ook"
msgstr ""
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr ""
msgid "Move the note to a notebook."
msgstr ""
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr ""
@@ -100,10 +49,17 @@ msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
#, javascript-format
msgid "No such command: %s"
msgstr ""
#, javascript-format
msgid "The command \"%s\" is only available in GUI mode"
msgstr ""
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr ""
@@ -161,6 +117,35 @@ msgstr ""
msgid "Note is not a to-do: \"%s\""
msgstr ""
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
msgid "Enter master password:"
msgstr ""
msgid "Operation cancelled"
msgstr ""
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
msgid "Completed decryption."
msgstr ""
msgid "Enabled"
msgstr ""
msgid "Disabled"
msgstr ""
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr ""
@@ -178,6 +163,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -201,10 +190,16 @@ msgstr ""
msgid "Displays usage information."
msgstr ""
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr ""
msgid "Type `help [command]` for more information about a command."
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
msgid "The possible commands are:"
@@ -234,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
msgstr ""
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
msgid "Imports an Evernote notebook file (.enex file)."
@@ -339,8 +334,7 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation."
msgstr ""
#, javascript-format
msgid "Delete notebook \"%s\"?"
msgid "Delete notebook? All notes within this notebook will also be deleted."
msgstr ""
msgid "Deletes the notes matching <note-pattern>."
@@ -359,7 +353,12 @@ msgstr ""
msgid "Searches for the given <pattern> in all the notes."
msgstr ""
msgid "Sets the property <name> of the given <note> to the given [value]."
#, javascript-format
msgid ""
"Sets the property <name> of the given <note> to the given [value]. Possible "
"properties are:\n"
"\n"
"%s"
msgstr ""
msgid "Displays summary about the notes and notebooks."
@@ -371,6 +370,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -381,10 +388,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -469,6 +472,22 @@ msgstr ""
msgid "Search:"
msgstr ""
msgid ""
"Welcome to Joplin!\n"
"\n"
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
"for usage information.\n"
"\n"
"For example, to create a notebook press `mb`; to create a note press `mn`."
msgstr ""
msgid ""
"One or more items are currently encrypted and you may need to supply a "
"master password. To do so please type `e2ee decrypt`. If you have already "
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
msgid "File"
msgstr ""
@@ -487,6 +506,10 @@ msgstr ""
msgid "Evernote Export Files"
msgstr ""
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr ""
@@ -505,10 +528,22 @@ msgstr ""
msgid "Search in all the notes"
msgstr ""
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr ""
msgid "Options"
msgid "Synchronisation status"
msgstr ""
msgid "Encryption options"
msgstr ""
msgid "General Options"
msgstr ""
msgid "Help"
@@ -517,6 +552,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -524,12 +562,112 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent encrypted to the sync target. Do not lose the "
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
msgid "Disable encryption"
msgstr ""
msgid "Enable encryption"
msgstr ""
msgid "Master Keys"
msgstr ""
msgid "Active"
msgstr ""
msgid "ID"
msgstr ""
msgid "Source"
msgstr ""
msgid "Created"
msgstr ""
msgid "Updated"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password OK"
msgstr ""
msgid ""
"Note: Only one master key is going to be used for encryption (the one marked "
"as \"active\"). Any of the keys might be used for decryption, depending on "
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
msgid "Encryption is:"
msgstr ""
msgid "Back"
msgstr ""
@@ -541,15 +679,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr ""
msgid "Note title:"
msgstr ""
msgid "Please create a notebook first"
msgstr ""
msgid "To-do title:"
msgstr ""
msgid "Notebook title:"
msgstr ""
@@ -565,9 +697,24 @@ msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Search"
msgstr ""
msgid "Layout"
msgstr ""
msgid "Some items cannot be synchronised."
msgstr ""
msgid "View them now"
msgstr ""
msgid "Some items cannot be decrypted."
msgstr ""
msgid "Set the password"
msgstr ""
msgid "Add or remove tags"
msgstr ""
@@ -583,6 +730,16 @@ msgstr ""
msgid "No notes in here. Create one by clicking on \"New note\"."
msgstr ""
msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
msgid "Open..."
msgstr ""
msgid "Save as..."
msgstr ""
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr ""
@@ -590,9 +747,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "to-do"
msgstr ""
msgid "note"
msgstr ""
#, javascript-format
msgid "Creating new %s..."
msgstr ""
msgid "Refresh"
msgstr ""
@@ -605,7 +775,13 @@ msgstr ""
msgid "Import"
msgstr ""
msgid "Delete notebook?"
msgid "Options"
msgstr ""
msgid "Synchronisation Status"
msgstr ""
msgid "Encryption Options"
msgstr ""
msgid "Remove this tag from all the notes?"
@@ -623,10 +799,10 @@ msgstr ""
msgid "Notebooks"
msgstr ""
msgid "Tags"
msgid "Searches"
msgstr ""
msgid "Searches"
msgid "Please select where the sync status should be exported to"
msgstr ""
#, javascript-format
@@ -640,12 +816,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -696,6 +878,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@@ -711,6 +897,12 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid "Encrypted"
msgstr ""
msgid "Encrypted items cannot be modified"
msgstr ""
msgid "Conflicts"
msgstr ""
@@ -736,14 +928,6 @@ msgstr ""
msgid "Cannot move note to \"%s\" notebook"
msgstr ""
msgid "File system synchronisation target directory"
msgstr ""
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
msgid "Text editor"
msgstr ""
@@ -770,16 +954,34 @@ msgstr ""
msgid "Dark"
msgstr ""
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Synchronisation interval"
msgid "When creating a new to-do:"
msgstr ""
msgid "Disabled"
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
#, javascript-format
@@ -794,9 +996,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -804,14 +1003,53 @@ msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
msgid "Nextcloud WebDAV URL"
msgstr ""
msgid "Nextcloud username"
msgstr ""
msgid "Nextcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "%s (%s): %s"
msgstr ""
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
msgid "Sync status (synced items / total items)"
msgstr ""
@@ -854,10 +1092,10 @@ msgstr ""
msgid "Log"
msgstr ""
msgid "Status"
msgid "Export Debug Report"
msgstr ""
msgid "Export Debug Report"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
@@ -870,6 +1108,9 @@ msgstr ""
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr ""
@@ -879,6 +1120,23 @@ msgstr ""
msgid "Cancel synchronisation"
msgstr ""
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, javascript-format
msgid "Created: %s"
msgstr ""
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
msgid "Enable"
msgstr ""
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr ""
@@ -886,6 +1144,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

1385
CliClient/locales/nl_BE.po Normal file

File diff suppressed because it is too large Load Diff

1358
CliClient/locales/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

1397
CliClient/locales/ru_RU.po Normal file

File diff suppressed because it is too large Load Diff

1316
CliClient/locales/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "joplin",
"version": "0.10.77",
"version": "1.0.98",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ajv": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz",
"integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"requires": {
"co": "4.6.0",
"fast-deep-equal": "1.0.0",
@@ -64,6 +64,11 @@
}
}
},
"async-mutex": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -85,6 +90,11 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
@@ -197,6 +207,11 @@
"delayed-stream": "1.0.0"
}
},
"compare-version": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
"integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -411,12 +426,12 @@
}
},
"fs-extra": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
"integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "3.0.1",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
@@ -437,7 +452,7 @@
"ndarray": "1.0.18",
"ndarray-pack": "1.2.1",
"node-bitmap": "0.0.1",
"omggif": "1.0.8",
"omggif": "1.0.9",
"parse-data-uri": "0.2.0",
"pngjs": "2.3.1",
"request": "2.83.0",
@@ -460,16 +475,6 @@
"assert-plus": "1.0.0"
}
},
"gettext-parser": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.0.tgz",
"integrity": "sha512-iloxjcw+uTPnQ8DrGICWtqkHNgk3mAiDI77pLmXQCnhM+BxFQXstzTA4zj3EpIYMysRQnnNzHyHzBUEazz80Sw==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"safe-buffer": "5.1.1"
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@@ -499,7 +504,7 @@
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"requires": {
"ajv": "5.3.0",
"ajv": "5.5.2",
"har-schema": "2.0.0"
}
},
@@ -738,9 +743,9 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsonfile": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
"integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "4.1.11"
}
@@ -797,12 +802,6 @@
"highlight.js": "9.12.0"
}
},
"marked": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=",
"dev": true
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -884,12 +883,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"mustache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz",
"integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=",
"dev": true
},
"nan": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
@@ -970,9 +963,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"omggif": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.8.tgz",
"integrity": "sha1-F483sqsLPXtG7ToORr0HkLWNNTA="
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.9.tgz",
"integrity": "sha1-3LcCTazVDFK00wPwSALJHAV8dl8="
},
"once": {
"version": "1.4.0",
@@ -1067,6 +1060,11 @@
"strict-uri-encode": "1.1.0"
}
},
"querystringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@@ -1121,6 +1119,11 @@
"uuid": "3.1.0"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
@@ -1937,9 +1940,9 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-kit": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.3.tgz",
"integrity": "sha512-G2T92klsuE+S9mqdKQyWurFweNQV5X+FRzSKTqYHRdaVUN/4dL6urbYJJ+xb9ep/4XWm+4RNT8j3acncNhFRBg==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.4.tgz",
"integrity": "sha512-imrOojdsXlL6xzfERCxvc/iA9Zwpzbfs+qeP6VB0s0rQVnMc3Nwkyhge0e8Uoayph7PVAwPNmLpohox27G3fgA==",
"requires": {
"xregexp": "3.2.0"
}
@@ -2036,15 +2039,15 @@
}
},
"terminal-kit": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.0.tgz",
"integrity": "sha512-ir0I2QtcBDSg2w0UvohlqdDpGlS3S2UYBG4NnYKnK/4VywgnbfxgdpXN3el0uCH3OeH6fG38luW7RmDM96FqUw==",
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.3.tgz",
"integrity": "sha512-ZHtuElnBhK0IXOYNvQ7eYgaArwEoOv7saQc4Q0Z9p02JeC7iajC20/odV77BKB3jw/Qthvf9mpASf8gNDYv7xQ==",
"requires": {
"async-kit": "2.2.3",
"get-pixels": "3.3.0",
"ndarray": "1.0.18",
"nextgen-events": "0.10.2",
"string-kit": "0.6.3",
"string-kit": "0.6.4",
"tree-kit": "0.5.26"
}
},
@@ -2054,16 +2057,16 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"tkwidgets": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.20.tgz",
"integrity": "sha512-9wGsMrrFJvE/6TKUc0dEFFhwxvZLeNsYOxnpy1JCwyk/hYCEF70nuvk7VvJeG4TPaQBaGKPj6c7pCgdREvz4Jw==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.25.tgz",
"integrity": "sha512-f+12QbxNCLg9Jou5JoPJxATGLmzpDAQeM7QRTXvuqdEB/QvPD9+UlPUL7eYJP1QJv2zzT6EIWWbdpDkXPEtzCQ==",
"requires": {
"chalk": "2.3.0",
"emphasize": "1.5.0",
"node-emoji": "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5",
"slice-ansi": "1.0.0",
"string-width": "2.1.1",
"terminal-kit": "1.14.0",
"terminal-kit": "1.14.3",
"wrap-ansi": "3.0.1"
},
"dependencies": {
@@ -2117,6 +2120,15 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
@@ -2161,6 +2173,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
}
},
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
},
"xregexp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",

View File

@@ -14,11 +14,12 @@
"title": "Joplin CLI",
"years": [
2016,
2017
2017,
2018
],
"owner": "Laurent Cozic"
},
"version": "0.10.77",
"version": "1.0.98",
"bin": {
"joplin": "./main.js"
},
@@ -27,9 +28,12 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
"fs-extra": "^3.0.1",
"fs-extra": "^5.0.0",
"html-entities": "^1.2.1",
"jssha": "^2.3.0",
"levenshtein": "^1.0.5",
@@ -54,16 +58,15 @@
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.20",
"tkwidgets": "^0.5.25",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"gettext-parser": "^1.2.2",
"jasmine": "^2.6.0",
"marked": "^0.3.6",
"mustache": "^2.3.0"
"jasmine": "^2.6.0"
},
"scripts": {
"test": "jasmine"

View File

@@ -9,4 +9,10 @@ bash $SCRIPT_DIR/build.sh
cp "$SCRIPT_DIR/package.json" build/
cp "$SCRIPT_DIR/../README.md" build/
cd "$SCRIPT_DIR/build"
npm publish
npm publish
NEW_VERSION=$(cat package.json | jq -r .version)
git add -A
git commit -m "CLI v$NEW_VERSION"
git tag "cli-v$NEW_VERSION"
git push && git push --tags

View File

@@ -4,4 +4,4 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
bash "$CLIENT_DIR/build.sh" && node "$CLIENT_DIR/build/main.js" --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
# bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/.config/joplin --stack-trace-enabled --log-level debug "$@"

View File

@@ -1,10 +1,15 @@
#!/bin/bash
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_DIR="$ROOT_DIR/tests-build"
TEST_FILE="$1"
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data"
npm test tests-build/synchronizer.js
if [[ $TEST_FILE == "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js)
else
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi

View File

@@ -0,0 +1,47 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const ArrayUtils = require('lib/ArrayUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
});
it('should remove array elements', async (done) => {
let a = ['un', 'deux', 'trois'];
a = ArrayUtils.removeElement(a, 'deux');
expect(a[0]).toBe('un');
expect(a[1]).toBe('trois');
expect(a.length).toBe(2);
a = ['un', 'deux', 'trois'];
a = ArrayUtils.removeElement(a, 'not in there');
expect(a.length).toBe(3);
done();
});
it('should find items using binary search', async (done) => {
let items = ['aaa', 'ccc', 'bbb'];
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
items.sort();
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(1);
expect(ArrayUtils.binarySearch(items, 'ccc')).toBe(2);
expect(ArrayUtils.binarySearch(items, 'oops')).toBe(-1);
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(0);
items = [];
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
done();
});
});

View File

@@ -0,0 +1,180 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = 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 { Database } = require('lib/database.js');
const Setting = require('lib/models/Setting.js');
const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
const MasterKey = require('lib/models/MasterKey');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const EncryptionService = require('lib/services/EncryptionService.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
let service = null;
describe('Encryption', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
//await setupDatabaseAndSynchronizer(2);
//await switchClient(1);
service = new EncryptionService();
BaseItem.encryptionService_ = service;
Setting.setValue('encryption.enabled', true);
done();
});
it('should encode and decode header', async (done) => {
const header = {
encryptionMethod: EncryptionService.METHOD_SJCL,
masterKeyId: '01234568abcdefgh01234568abcdefgh',
};
const encodedHeader = service.encodeHeader_(header);
const decodedHeader = service.decodeHeader_(encodedHeader);
delete decodedHeader.length;
expect(objectsEqual(header, decodedHeader)).toBe(true);
done();
});
it('should generate and decrypt a master key', async (done) => {
const masterKey = await service.generateMasterKey('123456');
expect(!!masterKey.checksum).toBe(true);
expect(!!masterKey.content).toBe(true);
let hasThrown = false;
try {
await service.decryptMasterKey(masterKey, 'wrongpassword');
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
expect(decryptedMasterKey.length).toBe(512);
done();
});
it('should encrypt and decrypt with a master key', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
// Test that a long string, that is going to be split into multiple chunks, encrypt
// and decrypt properly too.
let veryLongSecret = '';
for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9);
const cipherText2 = await service.encryptString(veryLongSecret);
const plainText2 = await service.decryptString(cipherText2);
expect(plainText2 === veryLongSecret).toBe(true);
done();
});
it('should fail to decrypt if master key not present', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
await service.unloadMasterKey(masterKey);
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
expect(hasThrown).toBe(true);
done();
});
it('should fail to decrypt if data tampered with', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
let cipherText = await service.encryptString('some secret');
cipherText += "ABCDEFGHIJ";
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
expect(hasThrown).toBe(true);
done();
});
it('should encrypt and decrypt notes and folders', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
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 });
let serialized = await Note.serializeForSync(note);
let deserialized = Note.filter(await Note.unserialize(serialized));
// Check that required properties are not encrypted
expect(deserialized.id).toBe(note.id);
expect(deserialized.parent_id).toBe(note.parent_id);
expect(deserialized.updated_time).toBe(note.updated_time);
// Check that at least title and body are encrypted
expect(!deserialized.title).toBe(true);
expect(!deserialized.body).toBe(true);
// Check that encrypted data is there
expect(!!deserialized.encryption_cipher_text).toBe(true);
encryptedNote = await Note.save(deserialized);
decryptedNote = await Note.decrypt(encryptedNote);
expect(decryptedNote.title).toBe(note.title);
expect(decryptedNote.body).toBe(note.body);
expect(decryptedNote.id).toBe(note.id);
expect(decryptedNote.parent_id).toBe(note.parent_id);
done();
});
it('should encrypt and decrypt files', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const sourcePath = __dirname + '/../tests/support/photo.jpg';
const encryptedPath = __dirname + '/data/photo.crypted';
const decryptedPath = __dirname + '/data/photo.jpg';
await service.encryptFile(sourcePath, encryptedPath);
await service.decryptFile(encryptedPath, decryptedPath);
expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false);
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
done();
});
});

View File

@@ -0,0 +1,32 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Setting = require('lib/models/Setting.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('models_Setting', function() {
beforeEach(async (done) => {
done();
});
it('should return only sub-values', asyncTest(async () => {
const settings = {
'sync.5.path': 'http://example.com',
'sync.5.username': 'testing',
}
let output = Setting.subValues('sync.5', settings);
expect(output['path']).toBe('http://example.com');
expect(output['username']).toBe('testing');
output = Setting.subValues('sync.4', settings);
expect('path' in output).toBe(false);
expect('username' in output).toBe(false);
}));
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,21 +1,25 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = 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 { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const Tag = require('lib/models/Tag.js');
const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const { BaseModel } = require('lib/base-model.js');
const Setting = require('lib/models/Setting.js');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 9000; // The first test is slow because the database needs to be built
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + 30000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@@ -23,7 +27,40 @@ async function allItems() {
return folders.concat(notes);
}
async function allSyncTargetItemsEncrypted() {
const list = await fileApi().list();
const files = list.items;
//console.info(Setting.value('resourceDir'));
let totalCount = 0;
let encryptedCount = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const remoteContentString = await fileApi().get(file.path);
const remoteContent = await BaseItem.unserialize(remoteContentString);
const ItemClass = BaseItem.itemClass(remoteContent);
if (!ItemClass.encryptionSupported()) continue;
totalCount++;
if (remoteContent.type_ === BaseModel.TYPE_RESOURCE) {
const content = await fileApi().get('.resource/' + remoteContent.id);
totalCount++;
if (content.substr(0, 5) === 'JED01') output = encryptedCount++;
}
if (!!remoteContent.encryption_applied) encryptedCount++;
}
if (!totalCount) throw new Error('No encryptable item on sync target');
return totalCount === encryptedCount;
}
async function localItemsSameAsRemote(locals, expect) {
let error = null;
try {
let files = await fileApi().list();
files = files.items;
@@ -38,31 +75,40 @@ async function localItemsSameAsRemote(locals, expect) {
expect(!!remote).toBe(true);
if (!remote) continue;
if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
} else {
expect(remote.updated_time).toBe(dbItem.updated_time);
}
// if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
// expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
// } else {
// expect(remote.updated_time).toBe(dbItem.updated_time);
// }
let remoteContent = await fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
expect(remoteContent.title).toBe(dbItem.title);
}
} catch (error) {
console.error(error);
} catch (e) {
error = e;
}
expect(error).toBe(null);
}
let insideBeforeEach = false;
describe('Synchronizer', function() {
beforeEach( async (done) => {
beforeEach(async (done) => {
insideBeforeEach = true;
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
insideBeforeEach = false;
});
it('should create remote items', async (done) => {
it('should create remote items', asyncTest(async () => {
let folder = await Folder.save({ title: "folder1" });
await Note.save({ title: "un", parent_id: folder.id });
@@ -71,11 +117,9 @@ describe('Synchronizer', function() {
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
}));
done();
});
it('should update remote item', async (done) => {
it('should update remote item', asyncTest(async () => {
let folder = await Folder.save({ title: "folder1" });
let note = await Note.save({ title: "un", parent_id: folder.id });
await synchronizer().start();
@@ -86,11 +130,9 @@ describe('Synchronizer', function() {
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
}));
done();
});
it('should create local items', async (done) => {
it('should create local items', asyncTest(async () => {
let folder = await Folder.save({ title: "folder1" });
await Note.save({ title: "un", parent_id: folder.id });
await synchronizer().start();
@@ -100,12 +142,11 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
await localItemsSameAsRemote(all, expect);
}));
done();
});
it('should update local items', async (done) => {
it('should update local items', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -130,11 +171,9 @@ describe('Synchronizer', function() {
let all = await allItems();
await localItemsSameAsRemote(all, expect);
}));
done();
});
it('should resolve note conflicts', async (done) => {
it('should resolve note conflicts', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -173,11 +212,9 @@ describe('Synchronizer', function() {
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
}
}));
done();
});
it('should resolve folders conflicts', async (done) => {
it('should resolve folders conflicts', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -208,11 +245,9 @@ describe('Synchronizer', function() {
let folder1_final = await Folder.load(folder1.id);
expect(folder1_final.title).toBe(folder1_modRemote.title);
}));
done();
});
it('should delete remote notes', async (done) => {
it('should delete remote notes', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -235,11 +270,9 @@ describe('Synchronizer', function() {
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
done();
});
it('should delete local notes', async (done) => {
it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -247,21 +280,44 @@ describe('Synchronizer', function() {
await switchClient(2);
await synchronizer().start();
await Note.delete(note1.id);
await Folder.delete(folder1.id);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
let items = await allItems();
expect(items.length).toBe(1);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
done();
});
}));
it('should delete remote folder', async (done) => {
it('should delete local notes', asyncTest(async () => {
// For these tests we pass the context around for each user. This is to make sure that the "deletedItemsProcessed"
// property of the basicDelta() function is cleared properly at the end of a sync operation. If it is not cleared
// it means items will no longer be deleted locally via sync.
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 });
let context1 = await synchronizer().start();
await switchClient(2);
let context2 = await synchronizer().start();
await Note.delete(note1.id);
context2 = await synchronizer().start({ context: context2 });
await switchClient(1);
context1 = await synchronizer().start({ context: context1 });
let items = await allItems();
expect(items.length).toBe(2);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
await Note.delete(note2.id);
context1 = await synchronizer().start({ context: context1 });
}));
it('should delete remote folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let folder2 = await Folder.save({ title: "folder2" });
await synchronizer().start();
@@ -277,12 +333,10 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
localItemsSameAsRemote(all, expect);
done();
});
await localItemsSameAsRemote(all, expect);
}));
it('should delete local folder', async (done) => {
it('should delete local folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let folder2 = await Folder.save({ title: "folder2" });
await synchronizer().start();
@@ -302,12 +356,10 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items = await allItems();
localItemsSameAsRemote(items, expect);
done();
});
await localItemsSameAsRemote(items, expect);
}));
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', async (done) => {
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
@@ -325,11 +377,9 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
expect(items[0].title).toBe('note1');
expect(items[0].is_conflict).toBe(1);
done();
});
}));
it('should resolve conflict if note has been deleted remotely and locally', async (done) => {
it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => {
let folder = await Folder.save({ title: "folder" });
let note = await Note.save({ title: "note", parent_id: folder.title });
await synchronizer().start();
@@ -349,12 +399,10 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
localItemsSameAsRemote(items, expect);
done();
});
await localItemsSameAsRemote(items, expect);
}));
it('should cross delete all folders', async (done) => {
it('should cross delete all folders', asyncTest(async () => {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
// 2 deletes item 2, they should both end up with no items after sync.
@@ -390,11 +438,9 @@ describe('Synchronizer', function() {
expect(items1.length).toBe(0);
expect(items1.length).toBe(items2.length);
done();
});
}));
it('should handle conflict when remote note is deleted then local note is modified', async (done) => {
it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -424,11 +470,9 @@ describe('Synchronizer', function() {
let unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
done();
});
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', async (done) => {
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let folder2 = await Folder.save({ title: "folder2" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
@@ -456,11 +500,9 @@ describe('Synchronizer', function() {
let items = await allItems();
expect(items.length).toBe(1);
done();
});
}));
it('should allow duplicate folder titles', async (done) => {
it('should allow duplicate folder titles', asyncTest(async () => {
let localF1 = await Folder.save({ title: "folder" });
await switchClient(2);
@@ -492,11 +534,15 @@ describe('Synchronizer', function() {
remoteF2 = await Folder.load(remoteF2.id);
expect(remoteF2.title == localF2.title).toBe(true);
}));
done();
});
async function shoudSyncTagTest(withEncryption) {
let masterKey = null;
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
masterKey = await loadEncryptionMasterKey();
}
it('should sync tags', async (done) => {
let f1 = await Folder.save({ title: "folder" });
let n1 = await Note.save({ title: "mynote" });
let n2 = await Note.save({ title: "mynote2" });
@@ -506,6 +552,12 @@ describe('Synchronizer', function() {
await switchClient(2);
await synchronizer().start();
if (withEncryption) {
const masterKey_2 = await MasterKey.load(masterKey.id);
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
let t = await Tag.load(tag.id);
await Tag.decrypt(t);
}
let remoteTag = await Tag.loadByTitle(tag.title);
expect(!!remoteTag).toBe(true);
expect(remoteTag.id).toBe(tag.id);
@@ -531,11 +583,17 @@ describe('Synchronizer', function() {
noteIds = await Tag.noteIds(tag.id);
expect(noteIds.length).toBe(1);
expect(remoteNoteIds[0]).toBe(noteIds[0]);
}
done();
});
it('should sync tags', asyncTest(async () => {
await shoudSyncTagTest(false);
}));
it('should not sync notes with conflicts', async (done) => {
it('should sync encrypted tags', asyncTest(async () => {
await shoudSyncTagTest(true);
}));
it('should not sync notes with conflicts', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });
let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
await synchronizer().start();
@@ -547,11 +605,9 @@ describe('Synchronizer', function() {
let folders = await Folder.all()
expect(notes.length).toBe(0);
expect(folders.length).toBe(1);
}));
done();
});
it('should not try to delete on remote conflicted notes that have been deleted', async (done) => {
it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
await synchronizer().start();
@@ -564,17 +620,13 @@ describe('Synchronizer', function() {
const deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
done();
});
it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => {
// That was previously a common conflict:
// - Client 1 mark todo as "done", and sync
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
// In theory it is a conflict because the todo_completed dates are different
// but in practice it doesn't matter, we can just take the date when the
// todo was marked as "done" the first time.
}));
async function ignorableNoteConflictTest(withEncryption) {
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
}
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
@@ -583,6 +635,10 @@ describe('Synchronizer', function() {
await switchClient(2);
await synchronizer().start();
if (withEncryption) {
await loadEncryptionMasterKey(null, true);
await decryptionWorker().start();
}
let note2 = await Note.load(note1.id);
note2.todo_completed = time.unixMs()-1;
await Note.save(note2);
@@ -597,35 +653,357 @@ describe('Synchronizer', function() {
note2conf = await Note.load(note1.id);
await synchronizer().start();
let conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(0);
if (!withEncryption) {
// That was previously a common conflict:
// - Client 1 mark todo as "done", and sync
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
// In theory it is a conflict because the todo_completed dates are different
// but in practice it doesn't matter, we can just take the date when the
// todo was marked as "done" the first time.
let notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].id).toBe(note1.id);
expect(notes[0].todo_completed).toBe(note2.todo_completed);
let conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(0);
done();
});
let notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].id).toBe(note1.id);
expect(notes[0].todo_completed).toBe(note2.todo_completed);
} else {
// If the notes are encrypted however it's not possible to do this kind of
// smart conflict resolving since we don't know the content, so in that
// case it's handled as a regular conflict.
it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => {
let conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(1);
let notes = await Note.all();
expect(notes.length).toBe(2);
}
}
it('should not consider it is a conflict if neither the title nor body of the note have changed', asyncTest(async () => {
await ignorableNoteConflictTest(false);
}));
it('should always handle conflict if local or remote are encrypted', asyncTest(async () => {
await ignorableNoteConflictTest(true);
}));
it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
await synchronizer().start();
await switchClient(2);
synchronizer().debugFlags_ = ['cancelDeltaLoop2'];
synchronizer().testingHooks_ = ['cancelDeltaLoop2'];
let context = await synchronizer().start();
let notes = await Note.all();
expect(notes.length).toBe(0);
synchronizer().debugFlags_ = [];
synchronizer().testingHooks_ = [];
await synchronizer().start({ context: context });
notes = await Note.all();
expect(notes.length).toBe(1);
}));
it('should skip items that cannot be synced', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
const noteId = note1.id;
await synchronizer().start();
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().testingHooks_ = ['rejectedByTarget'];
await synchronizer().start();
synchronizer().testingHooks_ = [];
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
await switchClient(2);
await synchronizer().start();
let notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].title).toBe('un');
await switchClient(1);
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(1);
}));
it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id });
await synchronizer().start();
// After synchronisation, remote items should be encrypted but local ones remain plain text
note1 = await Note.load(note1.id);
expect(note1.title).toBe('un');
await switchClient(2);
await synchronizer().start();
let folder1_2 = await Folder.load(folder1.id);
let note1_2 = await Note.load(note1.id);
let masterKey_2 = await MasterKey.load(masterKey.id);
// On this side however it should be received encrypted
expect(!note1_2.title).toBe(true);
expect(!folder1_2.title).toBe(true);
expect(!!note1_2.encryption_cipher_text).toBe(true);
expect(!!folder1_2.encryption_cipher_text).toBe(true);
// Master key is already encrypted so it does not get re-encrypted during sync
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);
// Get the decrypted items back
await Folder.decrypt(folder1_2);
await Note.decrypt(note1_2);
folder1_2 = await Folder.load(folder1.id);
note1_2 = await Note.load(note1.id);
// Check that properties match the original items. Also check
// the encryption did not affect the updated_time timestamp.
expect(note1_2.title).toBe(note1.title);
expect(note1_2.body).toBe(note1.body);
expect(note1_2.updated_time).toBe(note1.updated_time);
expect(!note1_2.encryption_cipher_text).toBe(true);
expect(folder1_2.title).toBe(folder1.title);
expect(folder1_2.updated_time).toBe(folder1.updated_time);
expect(!folder1_2.encryption_cipher_text).toBe(true);
}));
it('should enable encryption automatically when downloading new master key (and none was previously available)',asyncTest(async () => {
// Enable encryption on client 1 and sync an item
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
await switchClient(2);
// Synchronising should enable encryption since we're going to get a master key
expect(Setting.value('encryption.enabled')).toBe(false);
await synchronizer().start();
expect(Setting.value('encryption.enabled')).toBe(true);
// Check that we got the master key from client 1
const masterKey = (await MasterKey.all())[0];
expect(!!masterKey).toBe(true);
// Since client 2 hasn't supplied a password yet, no master key is currently loaded
expect(encryptionService().loadedMasterKeyIds().length).toBe(0);
// If we sync now, nothing should be sent to target since we don't have a password.
// Technically it's incorrect to set the property of an encrypted variable but it allows confirming
// that encryption doesn't work if user hasn't supplied a password.
await BaseItem.forceSync(folder1.id);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
folder1 = await Folder.load(folder1.id);
expect(folder1.title).toBe('folder1'); // Still at old value
await switchClient(2);
// Now client 2 set the master key password
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
// Now that master key should be loaded
expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id);
// Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted
await decryptionWorker().start();
folder1_2 = await Folder.save({ id: folder1.id, title: "change test" });
// If we sync now, this time client 1 should get the changes we did earlier
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
// Decrypt the data we just got
await decryptionWorker().start();
folder1 = await Folder.load(folder1.id);
expect(folder1.title).toBe('change test'); // Got title from client 2
}));
it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => {
// First create a folder, without encryption enabled, and sync it
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
let files = await fileApi().list()
let content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') >= 0).toBe(true)
// Then enable encryption and sync again
let masterKey = await encryptionService().generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizer().start();
// Even though the folder has not been changed it should have been synced again so that
// an encrypted version of it replaces the decrypted version.
files = await fileApi().list()
expect(files.items.length).toBe(2);
// By checking that the folder title is not present, we can confirm that the item has indeed been encrypted
// One of the two items is the master key
content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') < 0).toBe(true);
content = await fileApi().get(files.items[1].path);
expect(content.indexOf('folder1') < 0).toBe(true);
}));
it('should sync resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(500);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
let resourcePath1 = Resource.fullPath(resource1);
await synchronizer().start();
expect((await fileApi().list()).items.length).toBe(3);
await switchClient(2);
await synchronizer().start();
let allResources = await Resource.all();
expect(allResources.length).toBe(1);
let resource1_2 = allResources[0];
let resourcePath1_2 = Resource.fullPath(resource1_2);
expect(resource1_2.id).toBe(resource1.id);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
}));
it('should encryt resources', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
let resourcePath1 = Resource.fullPath(resource1);
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
let resource1_2 = (await Resource.all())[0];
resource1_2 = await Resource.decrypt(resource1_2);
let resourcePath1_2 = Resource.fullPath(resource1_2);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
}));
it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
let allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(true);
await encryptionService().disableEncryption();
await synchronizer().start();
allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(false);
}));
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', asyncTest(async () => {
// For some reason I can't explain, this test is sometimes executed before beforeEach is finished
// which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done.
while (insideBeforeEach) await time.msleep(100);
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
expect(Setting.value('encryption.enabled')).toBe(true);
// If we try to disable encryption now, it should throw an error because some items are
// currently encrypted. They must be decrypted first so that they can be sent as
// plain text to the sync target.
//let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
//expect(hasThrown).toBe(true);
// Now supply the password, and decrypt the items
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
await decryptionWorker().start();
// Try to disable encryption again
hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
expect(hasThrown).toBe(false);
// If we sync now the target should receive the decrypted items
await synchronizer().start();
allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(false);
}));
it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
await synchronizer().start();
expect(await allSyncTargetItemsEncrypted()).toBe(false);
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizer().start();
expect(await allSyncTargetItemsEncrypted()).toBe(true);
}));
it('should upload encrypted resource, but it should not mark the blob as encrypted locally', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizer().start();
let resource1 = (await Resource.all())[0];
expect(resource1.encryption_blob_encrypted).toBe(0);
}));
it('should create remote items with UTF-8 content', asyncTest(async () => {
let folder = await Folder.save({ title: "Fahrräder" });
await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id });
let all = await allItems();
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
}));
done();
});
});

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