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

Compare commits

...

175 Commits

Author SHA1 Message Date
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
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
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
0115e74163 All: Minor tweaks regarding encryption 2017-12-04 19:29:34 +00: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
7efeaa3a22 All: Fixed minor issue when logger not defined 2018-01-02 21:23:35 +01:00
Laurent Cozic
71e877d369 All: Improved encryption and synchronisation 2018-01-02 20:17:14 +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
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
08d2655f13 All: Various improvements to E2EE 2017-12-26 11:38:53 +01: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
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
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
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
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
Laurent Cozic
8a282fd2e1 All: Added fs drivers and encryption service 2017-12-11 23:52:42 +00:00
219 changed files with 9735 additions and 3373 deletions

4
.gitignore vendored
View File

@@ -35,4 +35,6 @@ _vieux/
_mydocs
.DS_Store
Assets/DownloadBadges*.psd
node_modules
node_modules
Tools/github_oauth_token.txt
_releases

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:
@@ -44,6 +47,6 @@ before_install:
script:
- |
cd ElectronClient/app
rsync -aP ../../ReactNativeClient/lib/ lib/
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install
yarn dist

View File

@@ -21,13 +21,15 @@ If you get a node-gyp related error you might need to manually install it: `npm
```
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.
@@ -44,7 +46,7 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
cd CliClient
npm install
./build.sh
rsync -aP ../ReactNativeClient/locales/ build/locales/
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');
@@ -65,6 +66,7 @@ class AppGui {
// a regular command it's not necessary since the process
// exits right away.
reg.setupRecurrentSync();
DecryptionWorker.instance().scheduleStart();
}
store() {
@@ -88,8 +90,8 @@ class AppGui {
return this.term().restoreState(state);
}
prompt(initialText = '', promptString = ':') {
return this.widget('statusBar').prompt(initialText, promptString);
prompt(initialText = '', promptString = ':', options = null) {
return this.widget('statusBar').prompt(initialText, promptString, options);
}
stdoutMaxWidth() {

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;
}
});
@@ -221,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_);
}
@@ -240,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_);
@@ -275,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); },
@@ -354,7 +354,7 @@ class Application extends BaseApplication {
this.dispatch({
type: 'TAG_UPDATE_ALL',
tags: tags,
items: tags,
});
this.store().dispatch({

View File

@@ -1,7 +1,7 @@
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 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');
@@ -48,7 +48,7 @@ async function handleAutocompletionPromise(line) {
if (options.length > 1 && options[1].indexOf(next) === 0) {
l.push(options[1]);
} else if (options[0].indexOf(next) === 0) {
l.push(options[2]);
l.push(options[0]);
}
}
if (l.length === 0) {

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;
@@ -79,7 +81,9 @@ class Command extends BaseCommand {
const termState = app().gui().termSaveState();
const spawnSync = require('child_process').spawnSync;
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
app().gui().termRestoreState(termState);
app().gui().hideModalOverlay();

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');

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 {

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,11 +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 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/base-item.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 {
@@ -35,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 {
@@ -61,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() {
@@ -120,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();

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 {

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,4 +1,4 @@
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');
@@ -44,7 +44,13 @@ class NoteWidget extends TextWidget {
} 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

@@ -42,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;
@@ -105,6 +106,8 @@ class StatusBarWidget extends BaseWidget {
this.term.showCursor(true);
const isSecurePrompt = !!this.promptState_.secure;
let options = {
cancelable: true,
history: this.history,
@@ -115,6 +118,7 @@ class StatusBarWidget extends BaseWidget {
};
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;
@@ -129,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);
}
}

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

@@ -4,6 +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"

View File

@@ -1,8 +1,8 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Laurent Cozic
# This file is distributed under the same license as the Joplin-CLI package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Laurent Cozic
# This file is distributed under the same license as the Joplin-CLI package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
@@ -116,6 +116,9 @@ msgstr "Ungültiger Befehl: %s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "Der Befehl \"%s\" ist nur im GUI Modus verfügbar"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Fehlendes benötigtes Argument: %s"
@@ -180,6 +183,36 @@ msgstr "Markiert ein To-Do als abgeschlossen."
msgid "Note is not a to-do: \"%s\""
msgstr "Notiz ist kein To-Do: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Notiz bearbeiten."
@@ -201,6 +234,10 @@ msgstr ""
"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um "
"zurück zum Terminal zu gelangen."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@@ -437,6 +474,16 @@ msgstr ""
"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target "
"Optionswert)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
@@ -450,12 +497,6 @@ msgstr ""
"Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und "
"fortfahren."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisationsziel: %s (%s)"
@@ -573,6 +614,13 @@ msgstr ""
"Um zum Beispiel ein Notizbuch zu erstellen, drücke `mb`; um eine Notiz zu "
"erstellen drücke `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 ""
msgid "File"
msgstr "Datei"
@@ -615,7 +663,11 @@ msgstr "Werkzeuge"
msgid "Synchronisation status"
msgstr "Status der Synchronisation"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Optionen"
msgid "Help"
@@ -703,13 +755,6 @@ msgstr "Status"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
msgid "Back"
msgstr "Zurück"
@@ -723,15 +768,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Bitte erstelle zuerst ein Notizbuch."
msgid "Note title:"
msgstr "Notizen Titel:"
msgid "Please create a notebook first"
msgstr "Bitte erstelle zuerst ein Notizbuch"
msgid "To-do title:"
msgstr "To-Do Titel:"
msgid "Notebook title:"
msgstr "Notizbuch Titel:"
@@ -808,6 +847,9 @@ msgstr "OneDrive Login"
msgid "Import"
msgstr "Importieren"
msgid "Options"
msgstr "Optionen"
msgid "Synchronisation Status"
msgstr "Synchronisations Status"
@@ -850,6 +892,9 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system"
msgstr "Dateisystem"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -914,6 +959,10 @@ msgstr "Lokale Objekte gelöscht: %d."
msgid "Deleted remote items: %d."
msgstr "Remote Objekte gelöscht: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Lokale Objekte erstellt: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Status: \"%s\"."
@@ -929,6 +978,13 @@ msgstr "Abgeschlossen: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Manche Objekte können nicht synchronisiert werden."
msgid "Conflicts"
msgstr "Konflikte"
@@ -990,6 +1046,12 @@ msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Synchronisation interval"
msgstr "Synchronisationsinterval"
@@ -1005,9 +1067,6 @@ msgstr "%d Stunde"
msgid "%d hours"
msgstr "%d Stunden"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Show advanced options"
msgstr "Erweiterte Optionen anzeigen"
@@ -1015,12 +1074,9 @@ msgid "Synchronisation target"
msgstr "Synchronisationsziel"
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 ""
"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem "
"Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um "
"den Zielpfad zu spezifizieren."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@@ -1032,6 +1088,15 @@ msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
@@ -1039,9 +1104,15 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
@@ -1090,6 +1161,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Fehlerbreicht exportieren"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Konfiguration"
@@ -1100,6 +1174,9 @@ msgstr "In Notizbuch verschieben..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Datum auswählen"
@@ -1109,6 +1186,25 @@ msgstr "Bestätigen"
msgid "Cancel synchronisation"
msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Erstellt: %d."
#, fuzzy
msgid "Password:"
msgstr "Passwort"
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Deaktiviert"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
@@ -1172,6 +1268,23 @@ msgstr ""
msgid "Welcome"
msgstr "Willkommen"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit "
#~ "dem Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2."
#~ "path`, um den Zielpfad zu spezifizieren."
#~ msgid "Note title:"
#~ msgstr "Notizen Titel:"
#~ msgid "To-do title:"
#~ msgstr "To-Do Titel:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""
#~ msgid "Delete notebook?"
#~ msgstr "Notizbuch löschen?"

View File

@@ -108,6 +108,9 @@ msgstr ""
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 ""
@@ -165,6 +168,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 ""
@@ -182,6 +214,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 ""
@@ -381,6 +417,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 ""
@@ -391,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -488,6 +528,13 @@ msgid ""
"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 ""
@@ -530,7 +577,10 @@ msgstr ""
msgid "Synchronisation status"
msgstr ""
msgid "Options"
msgid "Encryption options"
msgstr ""
msgid "General Options"
msgstr ""
msgid "Help"
@@ -614,12 +664,6 @@ msgstr ""
msgid "Encryption is:"
msgstr ""
msgid "Enabled"
msgstr ""
msgid "Disabled"
msgstr ""
msgid "Back"
msgstr ""
@@ -631,15 +675,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 ""
@@ -711,6 +749,9 @@ msgstr ""
msgid "Import"
msgstr ""
msgid "Options"
msgstr ""
msgid "Synchronisation Status"
msgstr ""
@@ -752,6 +793,9 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -808,6 +852,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@@ -823,6 +871,12 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid "Encrypted"
msgstr ""
msgid "Encrypted items cannot be modified"
msgstr ""
msgid "Conflicts"
msgstr ""
@@ -880,6 +934,12 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -895,9 +955,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -905,8 +962,8 @@ 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)"
@@ -917,6 +974,15 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -925,7 +991,13 @@ msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
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)"
@@ -973,6 +1045,9 @@ msgstr ""
msgid "Export Debug Report"
msgstr ""
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr ""
@@ -983,6 +1058,9 @@ msgstr ""
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr ""
@@ -992,6 +1070,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 ""

View File

@@ -110,6 +110,9 @@ msgstr "Comando inválido: \"%s\""
msgid "The command \"%s\" is only available in GUI mode"
msgstr "El comando \"%s\" unicamente disponible en modo GUI"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Falta un argumento requerido: %s"
@@ -172,6 +175,36 @@ msgstr "Marca una tarea como hecha."
msgid "Note is not a to-do: \"%s\""
msgstr "Una nota no es una tarea: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Deshabilitado"
msgid "Disabled"
msgstr "Deshabilitado"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Editar una nota."
@@ -191,6 +224,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -416,6 +453,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -429,10 +474,6 @@ msgstr ""
"curso, puedes eliminar el archivo de bloqueo en \"%s\" y reanudar la "
"operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -543,6 +584,13 @@ msgid ""
"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 "Archivo"
@@ -586,7 +634,11 @@ msgstr "Herramientas"
msgid "Synchronisation status"
msgstr "Sincronización de objetivo"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Opciones"
msgid "Help"
@@ -672,13 +724,6 @@ msgstr "Estatus"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Deshabilitado"
msgid "Disabled"
msgstr "Deshabilitado"
#, fuzzy
msgid "Back"
msgstr "Retroceder"
@@ -692,15 +737,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Por favor crea una libreta primero."
msgid "Note title:"
msgstr "Título de nota:"
msgid "Please create a notebook first"
msgstr "Por favor crea una libreta primero"
msgid "To-do title:"
msgstr "Títuto de lista de tareas:"
msgid "Notebook title:"
msgstr "Título de libreta:"
@@ -777,6 +816,9 @@ msgstr "Inicio de sesión de OneDrive"
msgid "Import"
msgstr "Importar"
msgid "Options"
msgstr "Opciones"
#, fuzzy
msgid "Synchronisation Status"
msgstr "Sincronización de objetivo"
@@ -820,6 +862,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -887,6 +932,10 @@ msgstr "Artículos locales borrados: %d."
msgid "Deleted remote items: %d."
msgstr "Artículos remotos borrados: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Artículos locales creados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: \"%s\"."
@@ -902,6 +951,13 @@ msgstr "Completado: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronizacion ya esta en progreso. Estod: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "No se puede inicializar sincronizador."
msgid "Conflicts"
msgstr "Conflictos"
@@ -964,6 +1020,12 @@ msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -979,22 +1041,16 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Show advanced options"
msgstr "Mostrar opciones "
msgid "Synchronisation target"
msgstr "Sincronización de objetivo"
#, fuzzy
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 ""
"El objetivo para sincronizarse a. Si sincronizando con el sistema de "
"archivos, establecer `sync.2.path` especifique el directorio destino."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -1006,6 +1062,15 @@ msgstr ""
"La ubicacion para sincronizar cuando el sistema de archivo tenga habilitada "
"la sincronización. Ver `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor inválido de opción: \"%s\". Los válores inválidos son: %s."
@@ -1013,8 +1078,14 @@ msgstr "Valor inválido de opción: \"%s\". Los válores inválidos son: %s."
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
@@ -1067,6 +1138,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Exportar reporte depuracion"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Configuracion"
@@ -1077,6 +1151,9 @@ msgstr "Mover a libreta...."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Mover %d notas a libreta \"%s\"?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Seleccionar fecha"
@@ -1086,6 +1163,24 @@ msgstr "Confirmar"
msgid "Cancel synchronisation"
msgstr "Sincronizacion cancelada"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Creado: %d."
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Deshabilitado"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Esta libreta no pudo ser guardada: %s"
@@ -1150,6 +1245,20 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#, fuzzy
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El objetivo para sincronizarse a. Si sincronizando con el sistema de "
#~ "archivos, establecer `sync.2.path` especifique el directorio destino."
#~ msgid "Note title:"
#~ msgstr "Título de nota:"
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"
#~ msgid "Delete notebook?"
#~ msgstr "Eliminar libreta?"

View File

@@ -111,6 +111,9 @@ msgstr "El comando no existe: %s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "El comando «%s» solamente está disponible en modo GUI"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Falta un argumento requerido: %s"
@@ -173,6 +176,36 @@ msgstr "Marca una tarea como hecha."
msgid "Note is not a to-do: \"%s\""
msgstr "Una nota no es una tarea: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Deshabilitado"
msgid "Disabled"
msgstr "Deshabilitado"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Editar una nota."
@@ -192,6 +225,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -425,6 +462,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -438,10 +483,6 @@ msgstr ""
"sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar "
"la operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -558,6 +599,13 @@ msgstr ""
"Por ejemplo, para crear una libreta escriba «mb», para crear una nota "
"escriba «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 ""
msgid "File"
msgstr "Archivo"
@@ -600,7 +648,11 @@ msgstr "Herramientas"
msgid "Synchronisation status"
msgstr "Estado de la sincronización"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Opciones"
msgid "Help"
@@ -684,13 +736,6 @@ msgstr "Estado"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Deshabilitado"
msgid "Disabled"
msgstr "Deshabilitado"
msgid "Back"
msgstr "Atrás"
@@ -702,15 +747,9 @@ msgstr "Se creará la nueva libreta «%s» y se importará en ella el archivo «
msgid "Please create a notebook first."
msgstr "Cree primero una libreta."
msgid "Note title:"
msgstr "Título de la nota:"
msgid "Please create a notebook first"
msgstr "Por favor crea una libreta primero"
msgid "To-do title:"
msgstr "Títuto de lista de tareas:"
msgid "Notebook title:"
msgstr "Título de libreta:"
@@ -783,6 +822,9 @@ msgstr "Inicio de sesión de OneDrive"
msgid "Import"
msgstr "Importar"
msgid "Options"
msgstr "Opciones"
msgid "Synchronisation Status"
msgstr "Estado de la sincronización"
@@ -824,6 +866,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -888,6 +933,10 @@ msgstr "Elementos locales borrados: %d."
msgid "Deleted remote items: %d."
msgstr "Elementos remotos borrados: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Elementos locales creados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: «%s»."
@@ -903,6 +952,13 @@ msgstr "Completado: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronización ya está en progreso. Estado: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "No se han podido sincronizar algunos de los elementos."
msgid "Conflicts"
msgstr "Conflictos"
@@ -963,6 +1019,12 @@ msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -978,9 +1040,6 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Show advanced options"
msgstr "Mostrar opciones avanzadas"
@@ -988,11 +1047,9 @@ msgid "Synchronisation target"
msgstr "Destino de sincronización"
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 ""
"El destino de la sincronización. Si se sincroniza con el sistema de "
"archivos, indique el directorio destino en «sync.2.path»."
msgid "Directory to synchronise with (absolute path)"
msgstr "Directorio con el que sincronizarse (ruta completa)"
@@ -1004,6 +1061,15 @@ msgstr ""
"La ruta a la que sincronizar cuando se activa la sincronización con sistema "
"de archivos. Vea «sync.target»."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1011,9 +1077,15 @@ msgstr "Opción inválida: «%s». Los valores posibles son: %s."
msgid "Items that cannot be synchronised"
msgstr "Elementos que no se pueden sincronizar"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "«%s»: «%s»"
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "Estado de sincronización (elementos sincronizados/elementos totales)"
@@ -1060,6 +1132,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Exportar informe de depuración"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Configuración"
@@ -1070,6 +1145,9 @@ msgstr "Mover a la libreta..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "¿Desea mover %d notas a libreta «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Seleccione fecha"
@@ -1079,6 +1157,24 @@ msgstr "Confirmar"
msgid "Cancel synchronisation"
msgstr "Cancelar sincronización"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Creado: %d."
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Deshabilitado"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "No se ha podido guardar esta libreta: %s"
@@ -1140,6 +1236,22 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El destino de la sincronización. Si se sincroniza con el sistema de "
#~ "archivos, indique el directorio destino en «sync.2.path»."
#~ msgid "Note title:"
#~ msgstr "Título de la nota:"
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "«%s»: «%s»"
#~ msgid "Delete notebook?"
#~ msgstr "Eliminar libreta?"

View File

@@ -100,15 +100,18 @@ msgstr "o"
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annulation de la synchronisation... Veuillez patienter."
#, fuzzy, javascript-format
#, javascript-format
msgid "No such command: %s"
msgstr "Commande invalide : \"%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"
@@ -171,6 +174,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."
@@ -192,6 +228,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."
@@ -221,11 +261,12 @@ msgstr "Affiche les informations d'utilisation."
msgid "Shortcuts are not available in CLI mode."
msgstr "Les raccourcis ne sont pas disponible en mode de ligne de commande."
#, fuzzy
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."
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 :"
@@ -379,6 +420,8 @@ msgstr "Supprimer le carnet sans demander la confirmation."
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>."
@@ -396,13 +439,17 @@ msgstr "Supprimer la note ?"
msgid "Searches for the given <pattern> in all the notes."
msgstr "Chercher le motif <pattern> dans toutes les notes."
#, fuzzy, javascript-format
#, 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."
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."
@@ -415,6 +462,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."
@@ -428,10 +485,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)"
@@ -538,6 +591,24 @@ msgid ""
"\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"
@@ -578,12 +649,14 @@ msgstr "Chercher dans toutes les notes"
msgid "Tools"
msgstr "Outils"
#, fuzzy
msgid "Synchronisation status"
msgstr "Cible de la synchronisation"
msgstr "État de la synchronisation"
msgid "Options"
msgstr "Options"
msgid "Encryption options"
msgstr "Options de cryptage"
msgid "General Options"
msgstr "Options générales"
msgid "Help"
msgstr "Aide"
@@ -602,20 +675,23 @@ msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annulation"
msgstr "Annuler"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgstr "Les notes et paramètres se trouve dans : %s"
msgid "Save"
msgstr ""
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 "
@@ -623,57 +699,57 @@ msgid ""
"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 ""
msgstr "Désactiver le cryptage"
msgid "Enable encryption"
msgstr ""
msgstr "Activer le cryptage"
msgid "Master Keys"
msgstr ""
msgstr "Clefs maître"
msgid "Active"
msgstr ""
msgstr "Actif"
msgid "ID"
msgstr ""
msgstr "ID"
msgid "Source"
msgstr ""
msgstr "Source"
#, fuzzy
msgid "Created"
msgstr "Créés : %d."
msgstr "Créé"
#, fuzzy
msgid "Updated"
msgstr "Mis à jour : %d."
msgstr "Mis à jour"
msgid "Password"
msgstr ""
msgstr "Mot de passe"
msgid "Password OK"
msgstr ""
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 "Status"
msgstr "État"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Désactivé"
msgid "Disabled"
msgstr "Désactivé"
msgstr "Le cryptage est :"
msgid "Back"
msgstr "Retour"
@@ -688,15 +764,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 :"
@@ -709,26 +779,23 @@ 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 "Layout"
msgstr "Disposition"
#, fuzzy
msgid "Some items cannot be synchronised."
msgstr "Impossible d'initialiser la synchronisation."
msgstr "Certains objets ne peuvent être synchronisés."
msgid "View them now"
msgstr ""
msgstr "Les voir maintenant"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Impossible d'initialiser la synchronisation."
msgstr "Certains objets ne peuvent être décryptés."
msgid "Set the password"
msgstr ""
msgstr "Définir le mot de passe"
msgid "Add or remove tags"
msgstr "Gérer les étiquettes"
@@ -746,12 +813,11 @@ 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\"."
#, fuzzy
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 le bouton "
"(+)"
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
"carnet\"."
#, javascript-format
msgid "Unsupported link or message: %s"
@@ -760,9 +826,8 @@ msgstr "Lien ou message non géré : %s"
msgid "Attach file"
msgstr "Attacher un fichier"
#, fuzzy
msgid "Set alarm"
msgstr "Définir ou modifier alarme"
msgstr "Régler alarme"
msgid "Refresh"
msgstr "Rafraîchir"
@@ -776,12 +841,14 @@ msgstr "Connexion OneDrive"
msgid "Import"
msgstr "Importer"
#, fuzzy
msgid "Options"
msgstr "Options"
msgid "Synchronisation Status"
msgstr "Cible de la synchronisation"
msgstr "État de la synchronisation"
msgid "Encryption Options"
msgstr ""
msgstr "Options de cryptage"
msgid "Remove this tag from all the notes?"
msgstr "Enlever cette étiquette de toutes les notes ?"
@@ -804,9 +871,9 @@ msgstr "Étiquettes"
msgid "Searches"
msgstr "Recherches"
#, fuzzy
msgid "Please select where the sync status should be exported to"
msgstr "Veuillez d'abord sélectionner un carnet."
msgstr ""
"Veuillez sélectionner un répertoire ou exporter l'état de la synchronisation"
#, javascript-format
msgid "Usage: %s"
@@ -819,6 +886,9 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
@@ -848,6 +918,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"
@@ -877,6 +953,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\"."
@@ -892,6 +972,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"
@@ -951,6 +1037,12 @@ msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
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"
@@ -966,9 +1058,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"
@@ -976,14 +1065,15 @@ 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 ""
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
@@ -992,16 +1082,34 @@ msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nextcloud : URL WebDAV"
msgid "Nexcloud username"
msgstr "Nextcloud : Nom utilisateur"
msgid "Nexcloud password"
msgstr "Nextcloud : 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 ""
msgstr "Objets qui ne peuvent pas être synchronisés"
#, javascript-format
msgid "\"%s\": \"%s\""
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)"
@@ -1050,6 +1158,9 @@ msgstr "Journal"
msgid "Export Debug Report"
msgstr "Exporter rapport de débogage"
msgid "Encryption Config"
msgstr "Config cryptage"
msgid "Configuration"
msgstr "Configuration"
@@ -1060,6 +1171,9 @@ msgstr "Déplacer la note vers carnet..."
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"
@@ -1069,6 +1183,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"
@@ -1105,10 +1236,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"
@@ -1131,6 +1262,19 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ 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 "Note title:"
#~ msgstr "Titre de la note :"
#~ msgid "To-do title:"
#~ msgstr "Titre de la tâche :"
#~ msgid "Delete notebook?"
#~ msgstr "Supprimer le carnet ?"
@@ -1205,9 +1349,6 @@ 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."

View File

@@ -116,6 +116,9 @@ msgstr "Ne postoji naredba: %s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "Naredba \"%s\" postoji samo u inačici s grafičkim sučeljem"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Nedostaje obavezni argument: %s"
@@ -180,6 +183,36 @@ msgstr "Označava zadatak završenim."
msgid "Note is not a to-do: \"%s\""
msgstr "Bilješka nije zadatak: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Onemogućeno"
msgid "Disabled"
msgstr "Onemogućeno"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Uredi bilješku."
@@ -202,6 +235,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Počinjem uređivati bilješku. Za povratak u naredbeni redak, zatvori uređivač."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Bilješka je spremljena."
@@ -433,6 +470,16 @@ msgstr "Sinkronizira sa udaljenom pohranom podataka."
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)"
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sinkronizacija je već u toku."
@@ -444,12 +491,6 @@ msgid ""
msgstr ""
"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi..."
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Meta sinkronizacije: %s (%s)"
@@ -566,6 +607,13 @@ msgstr ""
"\n"
"For example, to create a notebook press `mb`; to create a note press `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 ""
msgid "File"
msgstr "Datoteka"
@@ -608,7 +656,11 @@ msgstr "Alati"
msgid "Synchronisation status"
msgstr "Status sinkronizacije"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Opcije"
msgid "Help"
@@ -692,13 +744,6 @@ msgstr "Status"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Onemogućeno"
msgid "Disabled"
msgstr "Onemogućeno"
msgid "Back"
msgstr "Natrag"
@@ -712,15 +757,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Prvo stvori bilježnicu."
msgid "Note title:"
msgstr "Naslov bilješke:"
msgid "Please create a notebook first"
msgstr "Prvo stvori bilježnicu"
msgid "To-do title:"
msgstr "Naslov zadatka:"
msgid "Notebook title:"
msgstr "Naslov bilježnice:"
@@ -793,6 +832,9 @@ msgstr "OneDrive Login"
msgid "Import"
msgstr "Uvoz"
msgid "Options"
msgstr "Opcije"
msgid "Synchronisation Status"
msgstr "Status Sinkronizacije"
@@ -834,6 +876,9 @@ msgstr "Nepoznata zastavica: %s"
msgid "File system"
msgstr "Datotečni sustav"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -896,6 +941,10 @@ msgstr "Obrisane lokalne stavke: %d."
msgid "Deleted remote items: %d."
msgstr "Obrisane udaljene stavke: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Stvorene lokalne stavke: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Stanje: \"%s\"."
@@ -911,6 +960,13 @@ msgstr "Dovršeno: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinkronizacija je već u toku. Stanje: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Neke stavke se ne mogu sinkronizirati."
msgid "Conflicts"
msgstr "Sukobi"
@@ -970,6 +1026,12 @@ msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Save geo-location with notes"
msgstr "Spremi geolokacijske podatke sa bilješkama"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Synchronisation interval"
msgstr "Interval sinkronizacije"
@@ -985,9 +1047,6 @@ msgstr "%d sat"
msgid "%d hours"
msgstr "%d sati"
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Show advanced options"
msgstr "Prikaži napredne opcije"
@@ -995,11 +1054,9 @@ msgid "Synchronisation target"
msgstr "Sinkroniziraj sa"
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 ""
"Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
"sustavom, postavi `sync.2.path` na ciljani direktorij."
msgid "Directory to synchronise with (absolute path)"
msgstr "Direktorij za sinkroniziranje (apsolutna putanja)"
@@ -1011,6 +1068,15 @@ msgstr ""
"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa "
"datotečnim sustavom omogućena. Vidi `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@@ -1018,9 +1084,15 @@ msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
msgid "Items that cannot be synchronised"
msgstr "Stavke koje se ne mogu sinkronizirati"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "Status (sinkronizirane stavke / ukupni broj stavki)"
@@ -1067,6 +1139,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Izvezi Debug izvještaj"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Konfiguracija"
@@ -1077,6 +1152,9 @@ msgstr "Premjesti u bilježnicu..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Premjesti %d bilješke u bilježnicu \"%s\"?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Odaberi datum"
@@ -1086,6 +1164,24 @@ msgstr "Potvrdi"
msgid "Cancel synchronisation"
msgstr "Prekini sinkronizaciju"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Stvoreno: %d."
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Onemogućeno"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Bilježnicu nije moguće snimiti: %s"
@@ -1145,3 +1241,19 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
msgid "Welcome"
msgstr "Dobro došli"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
#~ "sustavom, postavi `sync.2.path` na ciljani direktorij."
#~ msgid "Note title:"
#~ msgstr "Naslov bilješke:"
#~ msgid "To-do title:"
#~ msgstr "Naslov zadatka:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""

View File

@@ -112,6 +112,9 @@ msgstr "Nessun comando: %s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "Il comando \"%s\" è disponibile solo nella modalità grafica"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Argomento richiesto mancante: %s"
@@ -174,6 +177,36 @@ msgstr "Segna un'attività come completata."
msgid "Note is not a to-do: \"%s\""
msgstr "La nota non è un'attività: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Disabilitato"
msgid "Disabled"
msgstr "Disabilitato"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Modifica nota."
@@ -193,6 +226,10 @@ msgstr "Non esiste la nota: \"%s\". Desideri crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Comincia a modificare la nota. Chiudi l'editor per tornare al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota è stata salvata."
@@ -416,6 +453,16 @@ msgstr ""
"Sincronizza con l'obiettivo fornito (come predefinito il valore di "
"configurazione sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "La sincronizzazione è in corso."
@@ -429,12 +476,6 @@ msgstr ""
"sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e "
"riprendere l'operazione."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Posizione di sincronizzazione: %s (%s)"
@@ -544,6 +585,13 @@ msgid ""
"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 "File"
@@ -586,7 +634,11 @@ msgstr "Strumenti"
msgid "Synchronisation status"
msgstr "Stato di sincronizzazione"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Opzioni"
msgid "Help"
@@ -672,13 +724,6 @@ msgstr "Stato"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Disabilitato"
msgid "Disabled"
msgstr "Disabilitato"
msgid "Back"
msgstr "Indietro"
@@ -690,15 +735,9 @@ msgstr "Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato"
msgid "Please create a notebook first."
msgstr "Per favore prima crea un blocco note."
msgid "Note title:"
msgstr "Titolo della Nota:"
msgid "Please create a notebook first"
msgstr "Per favore prima crea un blocco note"
msgid "To-do title:"
msgstr "Titolo dell'attività:"
msgid "Notebook title:"
msgstr "Titolo del blocco note:"
@@ -772,6 +811,9 @@ msgstr "Login OneDrive"
msgid "Import"
msgstr "Importa"
msgid "Options"
msgstr "Opzioni"
msgid "Synchronisation Status"
msgstr "Stato della Sincronizzazione"
@@ -814,6 +856,9 @@ msgstr "Etichetta sconosciuta: %s"
msgid "File system"
msgstr "File system"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -878,6 +923,10 @@ msgstr "Elementi locali eliminati: %d."
msgid "Deleted remote items: %d."
msgstr "Elementi remoti eliminati: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Elementi locali creati: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Stato: \"%s\"."
@@ -893,6 +942,13 @@ msgstr "Completata: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronizzazione è già in corso. Stato: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Alcuni elementi non possono essere sincronizzati."
msgid "Conflicts"
msgstr "Conflitti"
@@ -952,6 +1008,12 @@ msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Save geo-location with notes"
msgstr "Salva geo-localizzazione con le note"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Synchronisation interval"
msgstr "Intervallo di sincronizzazione"
@@ -967,9 +1029,6 @@ msgstr "%d ora"
msgid "%d hours"
msgstr "%d ore"
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Show advanced options"
msgstr "Mostra opzioni avanzate"
@@ -977,12 +1036,9 @@ msgid "Synchronisation target"
msgstr "Destinazione di sincronizzazione"
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 destinazione della sincronizzazione. Se si sincronizza con il file "
"system, impostare ' Sync. 2. Path ' per specificare la directory di "
"destinazione."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -994,6 +1050,15 @@ msgstr ""
"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
@@ -1001,9 +1066,15 @@ msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
msgid "Items that cannot be synchronised"
msgstr "Elementi che non possono essere sincronizzati"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "Stato di sincronizzazione (Elementi sincronizzati / Elementi totali)"
@@ -1050,6 +1121,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Esporta il Report di Debug"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Configurazione"
@@ -1060,6 +1134,9 @@ msgstr "Sposta sul blocco note..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Spostare le note %d sul blocco note \"%s\"?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Seleziona la data"
@@ -1069,6 +1146,24 @@ msgstr "Conferma"
msgid "Cancel synchronisation"
msgstr "Cancella la sincronizzazione"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Creato: %d."
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Disabilitato"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Il blocco note non può essere salvato: %s"
@@ -1131,6 +1226,23 @@ msgstr ""
msgid "Welcome"
msgstr "Benvenuto"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "La destinazione della sincronizzazione. Se si sincronizza con il file "
#~ "system, impostare ' Sync. 2. Path ' per specificare la directory di "
#~ "destinazione."
#~ msgid "Note title:"
#~ msgstr "Titolo della Nota:"
#~ msgid "To-do title:"
#~ msgstr "Titolo dell'attività:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""
#~ msgid "Delete notebook?"
#~ msgstr "Eliminare il blocco note?"

View File

@@ -110,6 +110,9 @@ msgstr "コマンドが違います:%s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "コマンド \"%s\"は、GUIのみで有効です。"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "引数が足りません:%s"
@@ -171,6 +174,36 @@ msgstr "ToDoを完了として"
msgid "Note is not a to-do: \"%s\""
msgstr "ノートはToDoリストではありません:\"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "無効"
msgid "Disabled"
msgstr "無効"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "ノートを編集する。"
@@ -190,6 +223,10 @@ msgstr "\"%s\"というノートはありません。お作りいたしますか
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 "ノートは保存されました。"
@@ -413,6 +450,14 @@ msgstr "リモート保存領域と同期します。"
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "指定のターゲットと同期します。(標準: sync.targetの設定値)"
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 "同期はすでに実行中です。"
@@ -425,10 +470,6 @@ msgstr ""
"ロックファイルがすでに保持されています。同期作業が行われていない場合は、\"%s"
"\"にあるロックファイルを削除して、作業を再度行ってください。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "認証は完了していません(認証トークンが得られませんでした)"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同期先: %s (%s)"
@@ -543,6 +584,13 @@ msgstr ""
"例えば、ノートブックの作成には`mb`で出来、ノートの作成は`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 ""
msgid "File"
msgstr "ファイル"
@@ -585,7 +633,11 @@ msgstr "ツール"
msgid "Synchronisation status"
msgstr "同期状況"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "オプション"
msgid "Help"
@@ -673,13 +725,6 @@ msgstr "状態"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "無効"
msgid "Disabled"
msgstr "無効"
msgid "Back"
msgstr "戻る"
@@ -693,15 +738,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 "ToDoの題名:"
msgid "Notebook title:"
msgstr "ノートブックの題名:"
@@ -774,6 +813,9 @@ msgstr "OneDriveログイン"
msgid "Import"
msgstr "インポート"
msgid "Options"
msgstr "オプション"
msgid "Synchronisation Status"
msgstr "同期状況"
@@ -815,6 +857,9 @@ msgstr "不明なフラグ: %s"
msgid "File system"
msgstr "ファイルシステム"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -879,6 +924,10 @@ msgstr "ローカルアイテムの削除: %d."
msgid "Deleted remote items: %d."
msgstr "リモートアイテムの削除: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "ローカルアイテムの作成: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "状態: \"%s\"。"
@@ -894,6 +943,13 @@ msgstr "完了: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "同期作業はすでに実行中です。状態: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "いくつかの項目は同期されませんでした。"
msgid "Conflicts"
msgstr "衝突"
@@ -955,6 +1011,12 @@ msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Synchronisation interval"
msgstr "同期間隔"
@@ -970,9 +1032,6 @@ msgstr "%d 時間"
msgid "%d hours"
msgstr "%d 時間"
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Show advanced options"
msgstr "詳細な設定の表示"
@@ -980,11 +1039,9 @@ 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 ""
"同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同期先"
"のディレクトリに設定してください。"
msgid "Directory to synchronise with (absolute path)"
msgstr "同期先のディレクトリ(絶対パス)"
@@ -996,6 +1053,15 @@ msgstr ""
"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく"
"ださい。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1004,7 +1070,13 @@ msgid "Items that cannot be synchronised"
msgstr "同期が出来なかったアイテム"
#, javascript-format
msgid "\"%s\": \"%s\""
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)"
@@ -1052,6 +1124,9 @@ msgstr "ログ"
msgid "Export Debug Report"
msgstr "デバッグレポートの出力"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "設定"
@@ -1062,6 +1137,9 @@ msgstr "ノートブックへ移動..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d個のノートを\"%s\"に移動しますか?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "日付の選択"
@@ -1071,6 +1149,24 @@ msgstr "確認"
msgid "Cancel synchronisation"
msgstr "同期の中止"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "作成しました:%d"
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "無効"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "ノートブックは保存できませんでした:%s"
@@ -1132,3 +1228,16 @@ msgstr ""
msgid "Welcome"
msgstr "ようこそ"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同"
#~ "期先のディレクトリに設定してください。"
#~ msgid "Note title:"
#~ msgstr "ノートの題名:"
#~ msgid "To-do title:"
#~ msgstr "ToDoの題名:"

View File

@@ -108,6 +108,9 @@ msgstr ""
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 ""
@@ -165,6 +168,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 ""
@@ -182,6 +214,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 ""
@@ -381,6 +417,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 ""
@@ -391,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -488,6 +528,13 @@ msgid ""
"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 ""
@@ -530,7 +577,10 @@ msgstr ""
msgid "Synchronisation status"
msgstr ""
msgid "Options"
msgid "Encryption options"
msgstr ""
msgid "General Options"
msgstr ""
msgid "Help"
@@ -614,12 +664,6 @@ msgstr ""
msgid "Encryption is:"
msgstr ""
msgid "Enabled"
msgstr ""
msgid "Disabled"
msgstr ""
msgid "Back"
msgstr ""
@@ -631,15 +675,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 ""
@@ -711,6 +749,9 @@ msgstr ""
msgid "Import"
msgstr ""
msgid "Options"
msgstr ""
msgid "Synchronisation Status"
msgstr ""
@@ -752,6 +793,9 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -808,6 +852,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@@ -823,6 +871,12 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid "Encrypted"
msgstr ""
msgid "Encrypted items cannot be modified"
msgstr ""
msgid "Conflicts"
msgstr ""
@@ -880,6 +934,12 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -895,9 +955,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -905,8 +962,8 @@ 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)"
@@ -917,6 +974,15 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -925,7 +991,13 @@ msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
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)"
@@ -973,6 +1045,9 @@ msgstr ""
msgid "Export Debug Report"
msgstr ""
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr ""
@@ -983,6 +1058,9 @@ msgstr ""
msgid "Move %d notes to notebook \"%s\"?"
msgstr ""
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr ""
@@ -992,6 +1070,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 ""

1267
CliClient/locales/nl_BE.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -109,6 +109,9 @@ msgstr "Comando inválido: \"%s\""
msgid "The command \"%s\" is only available in GUI mode"
msgstr "O comando \"%s\" está disponível somente em modo gráfico"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Argumento requerido faltando: %s"
@@ -171,6 +174,36 @@ msgstr "Marca uma tarefa como feita."
msgid "Note is not a to-do: \"%s\""
msgstr "Nota não é uma tarefa: \"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "Desabilitado"
msgid "Disabled"
msgstr "Desabilitado"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "Editar nota."
@@ -190,6 +223,10 @@ msgstr "A nota não existe: \"%s\". Criar?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Começando a editar a nota. Feche o editor para voltar ao prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Nota gravada."
@@ -412,6 +449,15 @@ msgstr ""
"Sincronizar para destino fornecido (p padrão é o valor de configuração sync."
"target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "A sincronização já está em andamento."
@@ -425,11 +471,6 @@ msgstr ""
"está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar "
"a operação."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Alvo de sincronização: %s (%s)"
@@ -538,6 +579,13 @@ msgid ""
"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 "Arquivo"
@@ -581,7 +629,11 @@ msgstr "Ferramentas"
msgid "Synchronisation status"
msgstr "Alvo de sincronização"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "Opções"
msgid "Help"
@@ -667,13 +719,6 @@ msgstr "Status"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Desabilitado"
msgid "Disabled"
msgstr "Desabilitado"
msgid "Back"
msgstr "Voltar"
@@ -686,15 +731,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Primeiro, crie um caderno."
msgid "Note title:"
msgstr "Título da nota:"
msgid "Please create a notebook first"
msgstr "Primeiro, crie um caderno"
msgid "To-do title:"
msgstr "Título da tarefa:"
msgid "Notebook title:"
msgstr "Título do caderno:"
@@ -769,6 +808,9 @@ msgstr "Login no OneDrive"
msgid "Import"
msgstr "Importar"
msgid "Options"
msgstr "Opções"
#, fuzzy
msgid "Synchronisation Status"
msgstr "Alvo de sincronização"
@@ -812,6 +854,9 @@ msgstr "Flag desconhecido: %s"
msgid "File system"
msgstr "Sistema de arquivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -876,6 +921,10 @@ msgstr "Itens locais excluídos: %d."
msgid "Deleted remote items: %d."
msgstr "Itens remotos excluídos: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Itens locais criados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: \"%s\"."
@@ -891,6 +940,13 @@ msgstr "Completado: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sincronização já em andamento. Estado: %s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Não é possível inicializar o sincronizador."
msgid "Conflicts"
msgstr "Conflitos"
@@ -951,6 +1007,12 @@ msgstr "Mostrar tarefas incompletas no topo das listas"
msgid "Save geo-location with notes"
msgstr "Salvar geolocalização com notas"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronização"
@@ -966,9 +1028,6 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Show advanced options"
msgstr "Mostrar opções avançadas"
@@ -976,11 +1035,9 @@ msgid "Synchronisation target"
msgstr "Alvo de sincronização"
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 ""
"O alvo para sincronizar. Se estiver sincronizando com o sistema de arquivos, "
"configure `sync.2.path` para especificar o diretório de destino."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -992,6 +1049,15 @@ msgstr ""
"O caminho para sincronizar, quando a sincronização do sistema de arquivos "
"está habilitada. Veja `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
@@ -999,8 +1065,14 @@ msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
@@ -1048,6 +1120,9 @@ msgstr "Log"
msgid "Export Debug Report"
msgstr "Exportar Relatório de Debug"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "Configuração"
@@ -1058,6 +1133,9 @@ msgstr "Mover para o caderno..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Mover %d notas para o caderno \"%s\"?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Selecionar data"
@@ -1067,6 +1145,24 @@ msgstr "Confirmar"
msgid "Cancel synchronisation"
msgstr "Cancelar sincronização"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Criado: %d."
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Desabilitado"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "O caderno não pôde ser salvo: %s"
@@ -1127,6 +1223,19 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome"
msgstr "Bem-vindo"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "O alvo para sincronizar. Se estiver sincronizando com o sistema de "
#~ "arquivos, configure `sync.2.path` para especificar o diretório de destino."
#~ msgid "Note title:"
#~ msgstr "Título da nota:"
#~ msgid "To-do title:"
#~ msgstr "Título da tarefa:"
#~ msgid "Delete notebook?"
#~ msgstr "Excluir caderno?"

View File

@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.4\n"
"X-Generator: Poedit 2.0.5\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -111,6 +111,9 @@ msgstr "Нет такой команды: %s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "Команда «%s» доступна только в режиме GUI"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "Отсутствует требуемый аргумент: %s"
@@ -173,6 +176,37 @@ msgstr "Отмечает задачу как завершённую."
msgid "Note is not a to-do: \"%s\""
msgstr "Заметка не является задачей: «%s»"
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
#, fuzzy
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 ""
#, fuzzy
msgid "Completed decryption."
msgstr "Включить шифрование"
msgid "Enabled"
msgstr "Включено"
msgid "Disabled"
msgstr "Отключено"
#, fuzzy, javascript-format
msgid "Encryption is: %s"
msgstr "Шифрование:"
msgid "Edit note."
msgstr "Редактировать заметку."
@@ -194,6 +228,10 @@ msgstr ""
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
"командной строке."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Заметка сохранена."
@@ -421,6 +459,14 @@ msgstr ""
"Синхронизация с заданной целью (по умолчанию — значение конфигурации sync."
"target)"
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 "Синхронизация уже выполняется."
@@ -434,10 +480,6 @@ msgstr ""
"производится, вы можете удалить файл блокировки в «%s» и возобновить "
"операцию."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Аутентификация не была завершена (не получен токен аутентификации)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Цель синхронизации: %s (%s)"
@@ -553,6 +595,13 @@ msgstr ""
"Например, для создания блокнота нужно ввести `mb`, для создания заметки — "
"`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 ""
msgid "File"
msgstr "Файл"
@@ -575,7 +624,7 @@ msgid "Quit"
msgstr "Выход"
msgid "Edit"
msgstr "Редактировать"
msgstr "Правка"
msgid "Copy"
msgstr "Копировать"
@@ -595,7 +644,12 @@ msgstr "Инструменты"
msgid "Synchronisation status"
msgstr "Статус синхронизации"
msgid "Options"
#, fuzzy
msgid "Encryption options"
msgstr "Настройки шифрования"
#, fuzzy
msgid "General Options"
msgstr "Настройки"
msgid "Help"
@@ -629,6 +683,9 @@ msgid ""
"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 "
@@ -636,18 +693,23 @@ msgid ""
"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 ""
msgstr "Отключить шифрование"
msgid "Enable encryption"
msgstr ""
msgstr "Включить шифрование"
msgid "Master Keys"
msgstr ""
msgstr "Мастер-ключи"
msgid "Active"
msgstr ""
msgstr "Активен"
msgid "ID"
msgstr "ID"
@@ -656,35 +718,32 @@ msgid "Source"
msgstr "Источник"
msgid "Created"
msgstr "Создана"
msgstr "Создан"
msgid "Updated"
msgstr "Обновлена"
msgstr "Обновлён"
msgid "Password"
msgstr ""
msgstr "Пароль"
msgid "Password OK"
msgstr ""
msgstr "Пароль 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 ""
"Внимание: Для шифрования может быть использован только один мастер-ключ "
"(отмеченный как «активный»). Для расшифровки может использоваться любой из "
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
"блокноты."
msgid "Status"
msgstr "Статус"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "Отключена"
msgid "Disabled"
msgstr "Отключена"
msgstr "Шифрование:"
msgid "Back"
msgstr "Назад"
@@ -697,15 +756,9 @@ msgstr "Будет создан новый блокнот «%s» и в него
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 "Название блокнота:"
@@ -730,12 +783,11 @@ msgstr "Некоторые элементы не могут быть синхр
msgid "View them now"
msgstr "Просмотреть их сейчас"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Некоторые элементы не могут быть синхронизированы."
msgstr "Некоторые элементы не могут быть расшифрованы."
msgid "Set the password"
msgstr ""
msgstr "Установить пароль"
msgid "Add or remove tags"
msgstr "Добавить или удалить теги"
@@ -778,11 +830,14 @@ msgstr "Вход в OneDrive"
msgid "Import"
msgstr "Импорт"
msgid "Options"
msgstr "Настройки"
msgid "Synchronisation Status"
msgstr "Статус синхронизации"
msgid "Encryption Options"
msgstr ""
msgstr "Настройки шифрования"
msgid "Remove this tag from all the notes?"
msgstr "Убрать этот тег со всех заметок?"
@@ -819,6 +874,9 @@ msgstr "Неизвестный флаг: %s"
msgid "File system"
msgstr "Файловая система"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -883,6 +941,10 @@ msgstr "Удалено локальных элементов: %d."
msgid "Deleted remote items: %d."
msgstr "Удалено удалённых элементов: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Создано локальных элементов: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Статус: «%s»."
@@ -898,6 +960,14 @@ msgstr "Завершено: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s"
#, fuzzy
msgid "Encrypted"
msgstr "Шифрование:"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Некоторые элементы не могут быть синхронизированы."
msgid "Conflicts"
msgstr "Конфликты"
@@ -957,6 +1027,12 @@ msgstr "Показывать незавершённые задачи вверх
msgid "Save geo-location with notes"
msgstr "Сохранять информацию о геолокации в заметках"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Synchronisation interval"
msgstr "Интервал синхронизации"
@@ -972,9 +1048,6 @@ msgstr "%d час"
msgid "%d hours"
msgstr "%d часов"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Show advanced options"
msgstr "Показывать расширенные настройки"
@@ -982,11 +1055,9 @@ 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 ""
"То, с чем будет осуществляться синхронизация. При синхронизации с файловой "
"системой в `sync.2.path` указывается целевой каталог."
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"
@@ -998,6 +1069,16 @@ msgstr ""
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
#, fuzzy
msgid "Nexcloud password"
msgstr "Установить пароль"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
@@ -1005,9 +1086,15 @@ msgstr "Неверное значение параметра: «%s». Досту
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "«%s»: «%s»"
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
@@ -1054,6 +1141,10 @@ msgstr "Лог"
msgid "Export Debug Report"
msgstr "Экспортировать отладочный отчёт"
#, fuzzy
msgid "Encryption Config"
msgstr "Шифрование:"
msgid "Configuration"
msgstr "Конфигурация"
@@ -1064,6 +1155,9 @@ msgstr "Переместить в блокнот..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "Переместить %d заметок в блокнот «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "Выбрать дату"
@@ -1073,6 +1167,25 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию"
#, fuzzy, javascript-format
msgid "Master Key %s"
msgstr "Мастер-ключи"
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "Создано: %d."
#, fuzzy
msgid "Password:"
msgstr "Пароль"
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "Включено"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "Не удалось сохранить блокнот: %s"
@@ -1132,3 +1245,19 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome"
msgstr "Добро пожаловать"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "То, с чем будет осуществляться синхронизация. При синхронизации с "
#~ "файловой системой в `sync.2.path` указывается целевой каталог."
#~ msgid "Note title:"
#~ msgstr "Название заметки:"
#~ msgid "To-do title:"
#~ msgstr "Название задачи:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "«%s»: «%s»"

View File

@@ -2,7 +2,7 @@
# Copyright (C) YEAR Laurent Cozic
# This file is distributed under the same license as the Joplin-CLI package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
#, fuzzy
msgid ""
msgstr ""
@@ -108,6 +108,9 @@ msgstr "无以下命令:%s"
msgid "The command \"%s\" is only available in GUI mode"
msgstr "命令\"%s\"仅在GUI模式下可用"
msgid "Cannot change encrypted item"
msgstr ""
#, javascript-format
msgid "Missing required argument: %s"
msgstr "缺失所需参数:%s"
@@ -168,6 +171,36 @@ msgstr "标记待办事项为完成。"
msgid "Note is not a to-do: \"%s\""
msgstr "笔记非待办事项:\"%s\""
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 ""
#, fuzzy
msgid "Enabled"
msgstr "已禁止"
msgid "Disabled"
msgstr "已禁止"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgid "Edit note."
msgstr "编辑笔记。"
@@ -185,6 +218,10 @@ msgstr "此笔记不存在:\"%s\"。是否创建?"
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 "笔记已被保存。"
@@ -393,6 +430,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 "同步正在进行中。"
@@ -405,10 +450,6 @@ msgstr ""
"锁定文件已被保留。若当前没有任何正在进行的同步,您可以在\"%s\"删除锁定文件并"
"继续操作。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "认证未完成(未收到认证令牌)。"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同步目标:%s (%s)"
@@ -510,6 +551,13 @@ msgid ""
"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 "文件"
@@ -552,7 +600,11 @@ msgstr "工具"
msgid "Synchronisation status"
msgstr "同步状态"
msgid "Options"
msgid "Encryption options"
msgstr ""
#, fuzzy
msgid "General Options"
msgstr "选项"
msgid "Help"
@@ -638,13 +690,6 @@ msgstr "状态"
msgid "Encryption is:"
msgstr ""
#, fuzzy
msgid "Enabled"
msgstr "已禁止"
msgid "Disabled"
msgstr "已禁止"
msgid "Back"
msgstr "返回"
@@ -656,15 +701,9 @@ msgstr "将创建新笔记本\"%s\"并将文件\"%s\"导入至其中"
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 "笔记本标题:"
@@ -738,6 +777,9 @@ msgstr "登陆OneDrive"
msgid "Import"
msgstr "导入"
msgid "Options"
msgstr "选项"
msgid "Synchronisation Status"
msgstr "同步状态"
@@ -780,6 +822,9 @@ msgstr "未知标记:%s"
msgid "File system"
msgstr "文件系统"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -841,6 +886,10 @@ msgstr "已删除本地项目: %d。"
msgid "Deleted remote items: %d."
msgstr "已删除远程项目: %d。"
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "已新建本地项目: %d。"
#, javascript-format
msgid "State: \"%s\"."
msgstr "状态:\"%s\"。"
@@ -856,6 +905,13 @@ msgstr "已完成:\"%s\""
msgid "Synchronisation is already in progress. State: %s"
msgstr "同步正在进行中。状态:%s"
msgid "Encrypted"
msgstr ""
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "一些项目无法被同步。"
msgid "Conflicts"
msgstr "冲突"
@@ -913,6 +969,12 @@ msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Synchronisation interval"
msgstr "同步间隔"
@@ -928,9 +990,6 @@ msgstr "%d小时"
msgid "%d hours"
msgstr "%d小时"
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Show advanced options"
msgstr "显示高级选项"
@@ -938,9 +997,9 @@ 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."
msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
"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 ""
@@ -950,6 +1009,15 @@ msgid ""
"See `sync.target`."
msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
@@ -957,9 +1025,15 @@ msgstr "无效的选项值:\"%s\"。可用值为:%s。"
msgid "Items that cannot be synchronised"
msgstr "项目无法被同步。"
#, javascript-format
msgid "\"%s\": \"%s\""
msgstr "\"%s\": \"%s\""
#, fuzzy, 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 ""
msgid "Sync status (synced items / total items)"
msgstr "同步状态(已同步项目/项目总数)"
@@ -1006,6 +1080,9 @@ msgstr "日志"
msgid "Export Debug Report"
msgstr "导出调试报告"
msgid "Encryption Config"
msgstr ""
msgid "Configuration"
msgstr "配置"
@@ -1016,6 +1093,9 @@ msgstr "移动至笔记本..."
msgid "Move %d notes to notebook \"%s\"?"
msgstr "移动%d条笔记至笔记本\"%s\"?"
msgid "Press to set the decryption password."
msgstr ""
msgid "Select date"
msgstr "选择日期"
@@ -1025,6 +1105,24 @@ msgstr "确认"
msgid "Cancel synchronisation"
msgstr "取消同步"
#, javascript-format
msgid "Master Key %s"
msgstr ""
#, fuzzy, javascript-format
msgid "Created: %s"
msgstr "已创建:%d条。"
msgid "Password:"
msgstr ""
msgid "Password cannot be empty"
msgstr ""
#, fuzzy
msgid "Enable"
msgstr "已禁止"
#, javascript-format
msgid "The notebook could not be saved: %s"
msgstr "此笔记本无法保存:%s"
@@ -1083,6 +1181,20 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
msgid "Welcome"
msgstr "欢迎"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
#~ msgid "Note title:"
#~ msgstr "笔记标题:"
#~ msgid "To-do title:"
#~ msgstr "待办事项标题:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""
#~ msgid "Delete notebook \"%s\"?"
#~ msgstr "删除笔记本\"%s\"?"

View File

@@ -1,13 +1,13 @@
{
"name": "joplin",
"version": "0.10.84",
"version": "0.10.90",
"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",
@@ -85,6 +85,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 +202,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 +421,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 +447,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",
@@ -489,7 +499,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"
}
},
@@ -728,9 +738,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"
}
@@ -948,9 +958,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",
@@ -1045,6 +1055,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",
@@ -1099,6 +1114,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",
@@ -1915,9 +1935,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"
}
@@ -2014,15 +2034,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"
}
},
@@ -2032,16 +2052,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.21",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.21.tgz",
"integrity": "sha512-gJfpYq3UM6AZ23ZM+D9BZ1PhsJLLHgjCOf487/lS9pO0uDdnkMcVXkkKEfRl00EyjPnGc88QZhEkVOvrtKsuPA==",
"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": {
@@ -2095,6 +2115,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",
@@ -2139,6 +2168,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.84",
"version": "0.10.90",
"bin": {
"joplin": "./main.js"
},
@@ -27,9 +28,11 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"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,9 +57,11 @@
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.20",
"tkwidgets": "^0.5.21",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {

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,17 @@
#!/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"
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
if [[ $TEST_FILE == "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
else
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi

View File

@@ -0,0 +1,32 @@
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('Encryption', 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();
});
});

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();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,14 +1,18 @@
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) => {
@@ -23,6 +27,38 @@ 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) {
try {
let files = await fileApi().list();
@@ -38,11 +74,11 @@ 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);
@@ -53,16 +89,22 @@ async function localItemsSameAsRemote(locals, expect) {
}
}
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 +113,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 +126,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();
@@ -102,11 +140,9 @@ describe('Synchronizer', function() {
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();
@@ -131,11 +167,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();
@@ -174,11 +208,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();
@@ -209,11 +241,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();
@@ -236,11 +266,27 @@ describe('Synchronizer', function() {
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
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();
it('should delete local notes', async (done) => {
await switchClient(2);
await synchronizer().start();
await Folder.delete(folder1.id);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
it('should delete local notes', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
@@ -258,11 +304,9 @@ describe('Synchronizer', function() {
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 remote folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let folder2 = await Folder.save({ title: "folder2" });
await synchronizer().start();
@@ -278,12 +322,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();
@@ -303,12 +345,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();
@@ -326,11 +366,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();
@@ -350,12 +388,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.
@@ -391,11 +427,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();
@@ -425,11 +459,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 });
@@ -457,11 +489,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);
@@ -493,11 +523,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" });
@@ -507,6 +541,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);
@@ -532,11 +572,15 @@ 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();
@@ -548,11 +592,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();
@@ -565,17 +607,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 });
@@ -584,6 +622,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);
@@ -598,48 +640,71 @@ 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);
}));
done();
});
it('items should skip items that cannot be synced', async (done) => {
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();
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['cannotSync'];
synchronizer().testingHooks_ = ['rejectedByTarget'];
await synchronizer().start();
synchronizer().debugFlags_ = [];
synchronizer().testingHooks_ = [];
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
await switchClient(2);
@@ -651,10 +716,271 @@ describe('Synchronizer', function() {
await switchClient(1);
disabledItems = await BaseItem.syncDisabledItems();
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(1);
}));
done();
});
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);
}));
});

View File

@@ -1,34 +1,47 @@
const fs = require('fs-extra');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { BaseModel } = require('lib/base-model.js');
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 { NoteTag } = require('lib/models/note-tag.js');
const BaseModel = require('lib/BaseModel.js');
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 NoteTag = require('lib/models/NoteTag.js');
const { Logger } = require('lib/logger.js');
const { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const Setting = require('lib/models/Setting.js');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { time } = require('lib/time-utils.js');
const { shimInit } = require('lib/shim-init-node.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const EncryptionService = require('lib/services/EncryptionService.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const WebDavApi = require('lib/WebDavApi');
let databases_ = [];
let synchronizers_ = [];
let encryptionServices_ = [];
let decryptionWorkers_ = [];
let fileApi_ = null;
let currentClient_ = 1;
shimInit();
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755);
@@ -36,22 +49,28 @@ fs.mkdirpSync(logDir, 0o755);
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
//const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const logger = new Logger();
logger.addTarget('console');
logger.addTarget('file', { path: logDir + '/log.txt' });
logger.setLevel(Logger.LEVEL_WARN);
logger.setLevel(Logger.LEVEL_WARN); // Set to INFO to display sync process in console
BaseItem.loadClass('Note', Note);
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');
@@ -79,10 +98,15 @@ async function switchClient(id) {
BaseItem.db_ = databases_[id];
Setting.db_ = databases_[id];
BaseItem.encryptionService_ = encryptionServices_[id];
Resource.encryptionService_ = encryptionServices_[id];
Setting.setConstant('resourceDir', resourceDir(id));
return Setting.load();
}
function clearDatabase(id = null) {
async function clearDatabase(id = null) {
if (id === null) id = currentClient_;
let queries = [
@@ -91,35 +115,46 @@ function clearDatabase(id = null) {
'DELETE FROM resources',
'DELETE FROM tags',
'DELETE FROM note_tags',
'DELETE FROM master_keys',
'DELETE FROM settings',
'DELETE FROM deleted_items',
'DELETE FROM sync_items',
];
return databases_[id].transactionExecBatch(queries);
await databases_[id].transactionExecBatch(queries);
}
function setupDatabase(id = null) {
async function setupDatabase(id = null) {
if (id === null) id = currentClient_;
Setting.cancelScheduleSave();
Setting.cache_ = null;
if (databases_[id]) {
return clearDatabase(id).then(() => {
return Setting.load();
});
await clearDatabase(id);
await Setting.load();
return;
}
const filePath = __dirname + '/data/test-' + id + '.sqlite';
return fs.unlink(filePath).catch(() => {
try {
await fs.unlink(filePath);
} catch (error) {
// Don't care if the file doesn't exist
}).then(() => {
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
// databases_[id].setLogger(logger);
// console.info(filePath);
return databases_[id].open({ name: filePath }).then(() => {
BaseModel.db_ = databases_[id];
return setupDatabase(id);
});
});
};
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
await databases_[id].open({ name: filePath });
BaseModel.db_ = databases_[id];
await Setting.load();
}
function resourceDir(id = null) {
if (id === null) id = currentClient_;
return __dirname + '/data/resources-' + id;
}
async function setupDatabaseAndSynchronizer(id = null) {
@@ -127,20 +162,26 @@ async function setupDatabaseAndSynchronizer(id = null) {
await setupDatabase(id);
EncryptionService.instance_ = null;
DecryptionWorker.instance_ = null;
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
syncTarget.setFileApi(fileApi());
syncTarget.setLogger(logger);
synchronizers_[id] = await syncTarget.synchronizer();
synchronizers_[id].autoStartDecryptionWorker_ = false; // For testing we disable this since it would make the tests non-deterministic
}
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
} else {
await fileApi().format();
}
encryptionServices_[id] = new EncryptionService();
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
await fileApi().clearRoot();
}
function db(id = null) {
@@ -153,6 +194,35 @@ function synchronizer(id = null) {
return synchronizers_[id];
}
function encryptionService(id = null) {
if (id === null) id = currentClient_;
return encryptionServices_[id];
}
function decryptionWorker(id = null) {
if (id === null) id = currentClient_;
return decryptionWorkers_[id];
}
async function loadEncryptionMasterKey(id = null, useExisting = false) {
const service = encryptionService(id);
let masterKey = null;
if (!useExisting) { // Create it
masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
} else { // Use the one already available
materKey = await MasterKey.all();
if (!materKey.length) throw new Error('No mater key available');
masterKey = materKey[0];
}
await service.loadMasterKey(masterKey, '123456', true);
return masterKey;
}
function fileApi() {
if (fileApi_) return fileApi_;
@@ -162,7 +232,17 @@ function fileApi() {
fileApi_ = new FileApi(syncDir, new FileApiDriverLocal());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
fileApi_ = new FileApi('/root', new FileApiDriverMemory());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('nextcloud')) {
const options = {
baseUrl: () => 'http://nextcloud.local/remote.php/dav/files/admin/JoplinTest',
username: () => 'admin',
password: () => '123456',
};
const api = new WebDavApi(options);
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
}
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
// let auth = require('./onedrive-auth.json');
// if (!auth) {
@@ -185,4 +265,43 @@ function fileApi() {
return fileApi_;
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId };
function objectsEqual(o1, o2) {
if (Object.getOwnPropertyNames(o1).length !== Object.getOwnPropertyNames(o2).length) return false;
for (let n in o1) {
if (!o1.hasOwnProperty(n)) continue;
if (o1[n] !== o2[n]) return false;
}
return true;
}
async function checkThrowAsync(asyncFn) {
let hasThrown = false;
try {
await asyncFn();
} catch (error) {
hasThrown = true;
}
return hasThrown;
}
function fileContentEqual(path1, path2) {
const fs = require('fs-extra');
const content1 = fs.readFileSync(path1, 'base64');
const content2 = fs.readFileSync(path2, 'base64');
return content1 === content2;
}
// Wrap an async test in a try/catch block so that done() is always called
// and display a proper error message instead of "unhandled promise error"
function asyncTest(callback) {
return async function(done) {
try {
await callback();
} catch (error) {
console.error(error);
}
done();
}
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };

View File

@@ -19,7 +19,8 @@
"title": "Demo for Joplin CLI",
"years": [
2016,
2017
2017,
2018
],
"owner": "Laurent Cozic"
},

View File

@@ -2,13 +2,14 @@ require('app-module-path').addPath(__dirname);
const { BaseApplication } = require('lib/BaseApplication');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim.js');
const { BaseModel } = require('lib/base-model.js');
const BaseModel = require('lib/BaseModel.js');
const MasterKey = require('lib/models/MasterKey');
const { _, setLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const { Tag } = require('lib/models/tag.js');
const Tag = require('lib/models/Tag.js');
const { reg } = require('lib/registry.js');
const { sprintf } = require('sprintf-js');
const { JoplinDatabase } = require('lib/joplin-database.js');
@@ -18,6 +19,7 @@ const { defaultState } = require('lib/reducer.js');
const packageInfo = require('./packageInfo.js');
const AlarmService = require('lib/services/AlarmService.js');
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
@@ -255,7 +257,7 @@ class Application extends BaseApplication {
name: 'search',
});
},
}]
}],
}, {
label: _('Tools'),
submenu: [{
@@ -266,15 +268,26 @@ class Application extends BaseApplication {
routeName: 'Status',
});
}
}, {
type: 'separator',
screens: ['Main'],
},{
label: _('Options'),
label: _('Encryption options'),
click: () => {
this.dispatch({
type: 'NAV_GO',
routeName: 'EncryptionConfig',
});
}
},{
label: _('General Options'),
click: () => {
this.dispatch({
type: 'NAV_GO',
routeName: 'Config',
});
}
}]
}],
}, {
label: _('Help'),
submenu: [{
@@ -288,7 +301,7 @@ class Application extends BaseApplication {
let message = [
p.description,
'',
'Copyright © 2016-2017 Laurent Cozic',
'Copyright © 2016-2018 Laurent Cozic',
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
];
bridge().showMessageBox({
@@ -354,7 +367,14 @@ class Application extends BaseApplication {
this.dispatch({
type: 'TAG_UPDATE_ALL',
tags: tags,
items: tags,
});
const masterKeys = await MasterKey.all();
this.dispatch({
type: 'MASTERKEY_UPDATE_ALL',
items: masterKeys,
});
this.store().dispatch({
@@ -387,6 +407,8 @@ class Application extends BaseApplication {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
AlarmService.updateAllNotifications();
DecryptionWorker.instance().scheduleStart();
});
}
}

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
@@ -89,24 +89,38 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, !value)
}
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
// There's probably a better way to do this but can't figure it out.
return (
<div key={key} style={rowStyle}>
<div key={key+value.toString()} style={rowStyle}>
<div style={controlStyle}>
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
</div>
</div>
);
} else if (md.type === Setting.TYPE_STRING) {
const onTextChange = (event) => {
const settings = Object.assign({}, this.state.settings);
settings[key] = event.target.value;
this.setState({ settings: settings });
updateSettingValue(key, event.target.value);
}
const inputType = md.secure === true ? 'password' : 'text';
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
<input type={inputType} style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
</div>
);
} else if (md.type === Setting.TYPE_INT) {
const onNumChange = (event) => {
updateSettingValue(key, event.target.value);
};
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
</div>
);
} else {

View File

@@ -0,0 +1,191 @@
const React = require('react');
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting');
const BaseItem = require('lib/models/BaseItem');
const EncryptionService = require('lib/services/EncryptionService');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const dialogs = require('./dialogs');
const shared = require('lib/components/shared/encryption-config-shared.js');
const pathUtils = require('lib/path-utils.js');
const { bridge } = require('electron').remote.require('./bridge');
class EncryptionConfigScreenComponent extends React.Component {
constructor() {
super();
shared.constructor(this);
}
componentDidMount() {
this.isMounted_ = true;
}
componentWillUnmount() {
this.isMounted_ = false;
}
initState(props) {
return shared.initState(this, props);
}
async refreshStats() {
return shared.refreshStats(this);
}
componentWillMount() {
this.initState(this.props);
}
componentWillReceiveProps(nextProps) {
this.initState(nextProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
renderMasterKey(mk) {
const theme = themeStyle(this.props.theme);
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
}
const onPasswordChange = (event) => {
return shared.onPasswordChange(this, mk, event.target.value);
}
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{active}</td>
<td style={theme.textStyle}>{mk.id}</td>
<td style={theme.textStyle}>{mk.source_application}</td>
<td style={theme.textStyle}>{time.formatMsToLocal(mk.created_time)}</td>
<td style={theme.textStyle}>{time.formatMsToLocal(mk.updated_time)}</td>
<td style={theme.textStyle}><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
<td style={theme.textStyle}>{passwordOk}</td>
</tr>
);
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const masterKeys = this.state.masterKeys;
const containerPadding = 10;
const headerStyle = {
width: style.width,
};
const containerStyle = {
padding: containerPadding,
overflow: 'auto',
height: style.height - theme.headerHeight - containerPadding * 2,
};
const mkComps = [];
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk));
}
const onToggleButtonClick = async () => {
const isEnabled = Setting.value('encryption.enabled');
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('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?'));
} else {
answer = await dialogs.prompt(_('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.'), '', '', { type: 'password' });
}
if (!answer) return;
try {
if (isEnabled) {
await EncryptionService.instance().disableEncryption();
} else {
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(answer);
}
} catch (error) {
await dialogs.alert(error.message);
}
}
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
const toggleButton = <button onClick={() => { onToggleButtonClick() }}>{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}</button>
let masterKeySection = null;
if (mkComps.length) {
masterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Master Keys')}</h1>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Source')}</th>
<th style={theme.textStyle}>{_('Created')}</th>
<th style={theme.textStyle}>{_('Updated')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Password OK')}</th>
</tr>
{mkComps}
</tbody>
</table>
<p style={theme.textStyle}>{_('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.')}</p>
</div>
);
}
return (
<div>
<Header style={headerStyle} />
<div style={containerStyle}>
<div style={{backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
Important: This is a <b>beta</b> feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain.
</p>
<p style={theme.textStyle}>
If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup <b>{pathUtils.toSystemSlashes(Setting.value('profileDir'), process.platform)}</b>
</p>
<p style={theme.textStyle}>
For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation: <a onClick={() => {bridge().openExternal('http://joplin.cozic.net/help/e2ee.html')}} href="#">http://joplin.cozic.net/help/e2ee.html</a>
</p>
</div>
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
{decryptedItemsInfo}
{toggleButton}
{masterKeySection}
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
};
};
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
module.exports = { EncryptionConfigScreen };

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { Folder } = require('lib/models/folder.js');
const Folder = require('lib/models/Folder.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');

View File

@@ -5,12 +5,12 @@ const { SideBar } = require('./SideBar.min.js');
const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const { Setting } = require('lib/models/setting.js');
const { BaseModel } = require('lib/base-model.js');
const { Tag } = require('lib/models/tag.js');
const { Note } = require('lib/models/note.js');
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const Tag = require('lib/models/Tag.js');
const Note = require('lib/models/Note.js');
const { uuid } = require('lib/uuid.js');
const { Folder } = require('lib/models/folder.js');
const Folder = require('lib/models/Folder.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const layoutUtils = require('lib/layout-utils.js');
@@ -44,16 +44,14 @@ class MainScreenComponent extends React.Component {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: title,
const newNote = {
parent_id: folderId,
is_todo: isTodo ? 1 : 0,
});
Note.updateGeolocation(note.id);
};
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
type: 'NOTE_SET_NEW_ONE',
item: newNote,
});
}
@@ -65,30 +63,14 @@ class MainScreenComponent extends React.Component {
return;
}
this.setState({
promptOptions: {
label: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, false);
} else if (command.name === 'newTodo') {
if (!this.props.folders.length) {
bridge().showErrorMessageBox(_('Please create a notebook first'));
return;
}
this.setState({
promptOptions: {
label: _('To-do title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, true);
} else if (command.name === 'newNotebook') {
this.setState({
promptOptions: {
@@ -132,7 +114,7 @@ class MainScreenComponent extends React.Component {
}
},
});
} else if (command.name === 'renameNotebook') {
} else if (command.name === 'renameFolder') {
const folder = await Folder.load(command.id);
if (!folder) return;
@@ -143,7 +125,8 @@ class MainScreenComponent extends React.Component {
onClose: async (answer) => {
if (answer !== null) {
try {
await Folder.save({ id: folder.id, title: answer }, { userSideValidation: true });
folder.title = answer;
await Folder.save(folder, { fields: ['title'], userSideValidation: true });
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
@@ -288,8 +271,7 @@ class MainScreenComponent extends React.Component {
const promptOptions = this.state.promptOptions;
const folders = this.props.folders;
const notes = this.props.notes;
const messageBoxVisible = this.props.hasDisabledSyncItems;
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
const theme = themeStyle(this.props.theme);
@@ -343,13 +325,31 @@ class MainScreenComponent extends React.Component {
});
}
const messageComp = messageBoxVisible ? (
<div style={styles.messageBox}>
<span style={theme.textStyle}>
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
</span>
</div>
) : null;
const onViewMasterKeysClick = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'EncryptionConfig',
});
}
let messageComp = null;
if (messageBoxVisible) {
let msg = null;
if (this.props.hasDisabledSyncItems) {
msg = <span>{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a></span>
} else if (this.props.showMissingMasterKeyMessage) {
msg = <span>{_('Some items cannot be decrypted.')} <a href="#" onClick={() => { onViewMasterKeysClick() }}>{_('Set the password')}</a></span>
}
messageComp = (
<div style={styles.messageBox}>
<span style={theme.textStyle}>
{msg}
</span>
</div>
);
}
return (
<div style={style}>
@@ -383,6 +383,7 @@ const mapStateToProps = (state) => {
folders: state.folders,
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
};
};

View File

@@ -3,6 +3,7 @@ const React = require('react');
const { connect } = require('react-redux');
const { time } = require('lib/time-utils.js');
const { themeStyle } = require('../theme.js');
const BaseModel = require('lib/BaseModel');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
@@ -53,26 +54,44 @@ class NoteListComponent extends React.Component {
}
itemContextMenu(event) {
const noteIds = this.props.selectedNoteIds;
const currentItemId = event.currentTarget.getAttribute('data-id');
if (!currentItemId) return;
let noteIds = [];
if (this.props.selectedNoteIds.indexOf(currentItemId) < 0) {
noteIds = [currentItemId];
} else {
noteIds = this.props.selectedNoteIds;
}
if (!noteIds.length) return;
const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id));
let hasEncrypted = false;
for (let i = 0; i < notes.length; i++) {
if (!!notes[i].encryption_applied) hasEncrypted = true;
}
const menu = new Menu()
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'setTags',
noteId: noteIds[0],
});
}}));
if (!hasEncrypted) {
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'setTags',
noteId: noteIds[0],
});
}}));
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
await Note.save(Note.toggleIsTodo(note));
eventManager.emit('noteTypeToggle', { noteId: note.id });
}
}}));
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
eventManager.emit('noteTypeToggle', { noteId: note.id });
}
}}));
}
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
@@ -120,14 +139,17 @@ class NoteListComponent extends React.Component {
id: item.id,
todo_completed: checked ? time.unixMs() : 0,
}
await Note.save(newNote);
await Note.save(newNote, { userSideValidation: true });
eventManager.emit('todoToggle', { noteId: item.id });
}
const hPadding = 10;
let style = Object.assign({ width: width }, this.style().listItem);
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected);
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) {
style = Object.assign(style, this.style().listItemSelected);
}
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
// but don't know how it will look in other OSes.
@@ -153,8 +175,9 @@ class NoteListComponent extends React.Component {
style={listItemTitleStyle}
onClick={(event) => { onTitleClick(event, item) }}
onDragStart={(event) => onDragStart(event) }
data-id={item.id}
>
{item.title}
{Note.displayTitle(item)}
</a>
</div>
}
@@ -162,8 +185,9 @@ class NoteListComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
let notes = this.props.notes.slice();
if (!this.props.notes.length) {
if (!notes.length) {
const padding = 10;
const emptyDivStyle = Object.assign({
padding: padding + 'px',
@@ -182,7 +206,7 @@ class NoteListComponent extends React.Component {
itemHeight={this.style().listItem.height}
style={style}
className={"note-list"}
items={this.props.notes}
items={notes}
itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } }
></ItemList>
);

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { Note } = require('lib/models/note.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { IconButton } = require('./IconButton.min.js');
const Toolbar = require('./Toolbar.min.js');
const { connect } = require('react-redux');
@@ -36,7 +36,13 @@ class NoteTextComponent extends React.Component {
isLoading: true,
webviewReady: false,
scrollHeight: null,
editorScrollTop: 0
editorScrollTop: 0,
newNote: null,
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
// to automatically set the title.
newAndNoTitleChangeNoteId: null,
};
this.lastLoadedNoteId_ = null;
@@ -75,7 +81,10 @@ class NoteTextComponent extends React.Component {
async componentWillMount() {
let note = null;
if (this.props.noteId) {
if (this.props.newNote) {
note = Object.assign({}, this.props.newNote);
} else if (this.props.noteId) {
note = await Note.load(this.props.noteId);
}
@@ -114,7 +123,14 @@ class NoteTextComponent extends React.Component {
}
async saveOneProperty(name, value) {
await shared.saveOneProperty(this, name, value);
if (this.state.note && !this.state.note.id) {
const note = Object.assign({}, this.state.note);
note[name] = value;
this.setState({ note: note });
this.scheduleSave();
} else {
await shared.saveOneProperty(this, name, value);
}
}
scheduleSave() {
@@ -128,17 +144,32 @@ class NoteTextComponent extends React.Component {
if (!options) options = {};
if (!('noReloadIfLocalChanges' in options)) options.noReloadIfLocalChanges = false;
const noteId = props.noteId;
this.lastLoadedNoteId_ = noteId;
const note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
if (!options.noReloadIfLocalChanges && this.isModified()) return;
await this.saveIfNeeded();
// If the note hasn't been changed, exit now
if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note);
delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return;
const previousNote = this.state.note ? Object.assign({}, this.state.note) : null;
const stateNoteId = this.state.note ? this.state.note.id : null;
let noteId = null;
let note = null;
let loadingNewNote = true;
if (props.newNote) {
note = Object.assign({}, props.newNote);
this.lastLoadedNoteId_ = null;
} else {
noteId = props.noteId;
loadingNewNote = stateNoteId !== noteId;
this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
if (options.noReloadIfLocalChanges && this.isModified()) return;
// If the note hasn't been changed, exit now
if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note);
delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return;
}
}
this.mdToHtml_ = null;
@@ -146,35 +177,53 @@ class NoteTextComponent extends React.Component {
// If we are loading nothing (noteId == null), make sure to
// set webviewReady to false too because the webview component
// is going to be removed in render().
const webviewReady = this.webview_ && this.state.webviewReady && noteId;
const webviewReady = this.webview_ && this.state.webviewReady && (noteId || props.newNote);
this.editorMaxScrollTop_ = 0;
// Scroll back to top when loading new note
if (loadingNewNote) {
this.editorMaxScrollTop_ = 0;
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
// and then (in the renderer callback) to the value we actually need. The first
// operation helps clear the scroll position cache. See:
// https://github.com/ajaxorg/ace/issues/2195
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
// and then (in the renderer callback) to the value we actually need. The first
// operation helps clear the scroll position cache. See:
// https://github.com/ajaxorg/ace/issues/2195
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
this.setState({
note: note,
lastSavedNote: Object.assign({}, note),
webviewReady: webviewReady,
});
}
async componentWillReceiveProps(nextProps) {
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
await this.reloadNote(nextProps);
if(this.editor_){
if (this.editor_) {
const session = this.editor_.editor.getSession();
const undoManager = session.getUndoManager();
undoManager.reset();
session.setUndoManager(undoManager);
this.editor_.editor.focus();
this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0);
}
}
let newState = {
note: note,
lastSavedNote: Object.assign({}, note),
webviewReady: webviewReady,
};
if (!note) {
newState.newAndNoTitleChangeNoteId = null;
} else if (note.id !== this.state.newAndNoTitleChangeNoteId) {
newState.newAndNoTitleChangeNoteId = null;
}
this.setState(newState);
}
async componentWillReceiveProps(nextProps) {
if (nextProps.newNote) {
await this.reloadNote(nextProps);
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
await this.reloadNote(nextProps);
}
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
await this.reloadNote(nextProps, { noReloadIfLocalChanges: true });
}
@@ -190,6 +239,7 @@ class NoteTextComponent extends React.Component {
title_changeText(event) {
shared.noteComponent_change(this, 'title', event.target.value);
this.setState({ newAndNoTitleChangeNoteId: null });
this.scheduleSave();
}
@@ -397,20 +447,10 @@ class NoteTextComponent extends React.Component {
menu.popup(bridge().window());
}
// shouldComponentUpdate(nextProps, nextState) {
// //console.info('NEXT PROPS', JSON.stringify(nextProps));
// console.info('NEXT STATE ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const note = this.state.note;
const body = note ? note.body : '';
const body = note && note.body ? note.body : '';
const theme = themeStyle(this.props.theme);
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
@@ -425,7 +465,7 @@ class NoteTextComponent extends React.Component {
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
if (!note) {
if (!note || !!note.encryption_applied) {
const emptyDivStyle = Object.assign({
backgroundColor: 'black',
opacity: 0.1,
@@ -537,7 +577,7 @@ class NoteTextComponent extends React.Component {
const titleEditor = <input
type="text"
style={titleEditorStyle}
value={note ? note.title : ''}
value={note && note.title ? note.title : ''}
onChange={(event) => { this.title_changeText(event); }}
/>
@@ -605,6 +645,7 @@ const mapStateToProps = (state) => {
theme: state.settings.theme,
showAdvancedOptions: state.settings.showAdvancedOptions,
syncStarted: state.syncStarted,
newNote: state.newNote,
};
};

View File

@@ -42,17 +42,20 @@ class PromptDialog extends React.Component {
this.styles_ = {};
const paddingTop = 20;
this.styles_.modalLayer = {
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height,
height: height - paddingTop,
backgroundColor: 'rgba(0,0,0,0.6)',
display: visible ? 'flex' : 'none',
alignItems: 'center',
alignItems: 'flex-start',
justifyContent: 'center',
paddingTop: paddingTop + 'px',
};
this.styles_.promptDialog = {
@@ -88,24 +91,6 @@ class PromptDialog extends React.Component {
return this.styles_;
}
// shouldComponentUpdate(nextProps, nextState) {
// console.info(JSON.stringify(nextProps)+JSON.stringify(nextState));
// console.info('NEXT PROPS ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// console.info('NEXT STATE ====================');
// for (var n in nextState) {
// if (!nextState.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextState[n] === this.state[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);

View File

@@ -4,13 +4,14 @@ const { createStore } = require('redux');
const { connect, Provider } = require('react-redux');
const { _ } = require('lib/locale.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { MainScreen } = require('./MainScreen.min.js');
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
const { StatusScreen } = require('./StatusScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js');
const { ConfigScreen } = require('./ConfigScreen.min.js');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js');
const { Navigator } = require('./Navigator.min.js');
const { app } = require('../app');
@@ -77,6 +78,7 @@ class RootComponent extends React.Component {
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') },
};
return (

View File

@@ -2,10 +2,10 @@ const React = require('react');
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { BaseModel } = require('lib/base-model.js');
const { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
const { bridge } = require('electron').remote.require('./bridge');
@@ -35,6 +35,7 @@ class SideBarComponent extends React.Component {
alignItems: 'center',
cursor: 'default',
opacity: 0.8,
whiteSpace: 'nowrap',
},
listItemSelected: {
backgroundColor: theme.selectedColor2,
@@ -107,6 +108,11 @@ class SideBarComponent extends React.Component {
const menu = new Menu();
let item = null;
if (itemType === BaseModel.TYPE_FOLDER) {
item = BaseModel.byId(this.props.folders, itemId);
}
menu.append(new MenuItem({label: _('Delete'), click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage);
if (!ok) return;
@@ -123,11 +129,11 @@ class SideBarComponent extends React.Component {
}
}}))
if (itemType === BaseModel.TYPE_FOLDER) {
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem({label: _('Rename'), click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'renameNotebook',
name: 'renameFolder',
id: itemId,
});
}}))
@@ -180,6 +186,8 @@ class SideBarComponent extends React.Component {
}
}
const itemTitle = Folder.displayTitle(folder);
return <a
className="list-item"
onDragOver={(event) => { onDragOver(event, folder) } }
@@ -189,14 +197,14 @@ class SideBarComponent extends React.Component {
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={(event) => this.itemContextMenu(event)}
key={folder.id}
style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}
style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle}
</a>
}
tagItem(tag, selected) {
let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected);
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{tag.title}</a>
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{Tag.displayTitle(tag)}</a>
}
searchItem(search, selected) {

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { Setting } = require('lib/models/setting.js');
const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
@@ -70,7 +70,9 @@ class StatusScreenComponent extends React.Component {
for (let n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
let text = section.body[n];
if (!text) text = '\xa0';
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{text}</div>);
}
return (

View File

@@ -0,0 +1,33 @@
const smalltalk = require('smalltalk');
class Dialogs {
async alert(message, title = '') {
await smalltalk.alert(title, message);
}
async confirm(message, title = '') {
try {
await smalltalk.confirm(title, message);
return true;
} catch (error) {
return false;
}
}
async prompt(message, title = '', defaultValue = '', options = null) {
if (options === null) options = {};
try {
const answer = await smalltalk.prompt(title, message, defaultValue, options);
return answer;
} catch (error) {
return null;
}
}
}
const dialogs = new Dialogs();
module.exports = dialogs;

View File

@@ -6,6 +6,23 @@
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
<style>
.smalltalk {
background-color: rgba(0,0,0,.5);
}
.smalltalk input {
margin-top: 1em;
}
.smalltalk .page {
max-width: 30em;
}
.ace_editor * {
/* Necessary to make sure Russian text is displayed properly */
/* https://github.com/laurent22/joplin/issues/155 */
font-family: monospace !important;
}
</style>
</head>
<body>
<div id="react-root"></div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ locales['fr_FR'] = require('./fr_FR.json');
locales['hr_HR'] = require('./hr_HR.json');
locales['it_IT'] = require('./it_IT.json');
locales['ja_JP'] = require('./ja_JP.json');
locales['nl_BE'] = require('./nl_BE.json');
locales['pt_BR'] = require('./pt_BR.json');
locales['ru_RU'] = require('./ru_RU.json');
locales['zh_CN'] = require('./zh_CN.json');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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