1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

..

190 Commits

Author SHA1 Message Date
Laurent Cozic
51fc2d8e51 CLI v1.0.98 2018-02-16 23:49:22 +00:00
Laurent Cozic
d87c192ff1 Updated readme and translation 2018-02-16 23:26:31 +00:00
Laurent Cozic
52ccf398a6 Update readme 2018-02-16 23:17:55 +00:00
Laurent Cozic
344d0e2687 Update readme 2018-02-16 23:14:26 +00:00
Laurent Cozic
1bc4d6b423 CLI v1.0.97 2018-02-16 23:08:57 +00:00
Laurent Cozic
baa9ca7ea3 Updated terminal readme 2018-02-16 23:08:04 +00:00
Laurent Cozic
e4d477fb4c CLI: Resolve #63: Allow customizing shortcuts 2018-02-16 22:53:53 +00:00
Laurent Cozic
71319eee28 Also add link to po file in translation table 2018-02-16 21:45:28 +00:00
Laurent Cozic
68b31526f8 Updated translations 2018-02-16 21:39:54 +00:00
Laurent Cozic
0b2b7324d9 Merge pull request #224 from fmrtn/master
Fixed and updated Spanish translation
2018-02-16 20:35:46 +00:00
Fernando
43512cf27b Fixed and updated Spanish translation 2018-02-16 20:36:58 +01:00
Laurent Cozic
4218b65969 Electron: Added several keyboard shortcuts 2018-02-16 18:08:02 +00:00
Laurent Cozic
7244e12b78 Electron: Fixed: Confirmation message boxes, and release notes text 2018-02-16 18:06:02 +00:00
Laurent Cozic
a796ef5c66 All: Fixed: Notify DecryptionWorker when item added due to conflict, to make sure it is decrypted as early as possible 2018-02-16 17:55:50 +00:00
Laurent Cozic
9474a05aaa Update readme 2018-02-16 00:17:05 +00:00
Laurent Cozic
41df355a7e CLI v1.0.96 2018-02-15 23:51:00 +00:00
Laurent Cozic
4f3ab87914 Electron release v1.0.64 2018-02-15 23:21:47 +00:00
Laurent Cozic
5d1a08707c Android release v1.0.98 2018-02-15 23:20:28 +00:00
Laurent Cozic
4f822df80e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 23:05:34 +00:00
Laurent Cozic
951be5cbf6 Electron: Fixes #201, Fixes #216: Make sure only one update check can run at a time, and improved modal dialog boxes 2018-02-15 23:05:04 +00:00
Laurent Cozic
b6c2341542 Merge pull request #219 from fmrtn/patch-1
Update README.md
2018-02-15 18:33:48 +00:00
Laurent Cozic
a6e6b49a9d Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:33:21 +00:00
Laurent Cozic
3a4bbd571e All: Provide Content-Length header for WebDAV for better compatibility with more servers 2018-02-15 18:33:08 +00:00
Laurent Cozic
feccc6150e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:19:11 +00:00
Laurent Cozic
a37b599a6b macOS: Allow copy and paste from config and encryption screen 2018-02-15 18:19:00 +00:00
Laurent Cozic
9347683fe3 All: Fixed Nextcloud sync target ID (which was incorrectly set to WebDAV sync ID) 2018-02-15 18:01:05 +00:00
Laurent Cozic
3551c26e28 Fixed race condition when testing with memory driver and fixed encoding issue 2018-02-15 17:12:09 +00:00
Fernando
cfca0107eb Update README.md 2018-02-15 13:09:59 +01:00
Laurent Cozic
81bc975193 Update readme 2018-02-14 21:54:34 +00:00
Laurent Cozic
7908fda451 Android release v1.0.97 2018-02-14 19:11:35 +00:00
Laurent Cozic
cdbb7c4b0d Merge branch 'master' of github.com:laurent22/joplin 2018-02-14 19:10:42 +00:00
Laurent Cozic
414e57ec55 Electron release v1.0.63 2018-02-14 19:08:24 +00:00
Laurent Cozic
1871123066 All: Improved WebDAV driver compatibility with some services (eg. Seafile) 2018-02-14 19:08:07 +00:00
Laurent Cozic
87bc08bef5 iOS: Fixed resource decryption issue, log page crash and text input rendering issue 2018-02-14 15:28:56 +00:00
Laurent Cozic
214a39c3d3 All: Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken. 2018-02-13 18:26:33 +00:00
Laurent Cozic
ef0cc5e33e Update readme 2018-02-12 20:23:16 +00:00
Laurent Cozic
3a1fa583ab CLI v1.0.95 2018-02-12 18:07:20 +00:00
Laurent Cozic
c1161ae017 Android release v1.0.95 2018-02-12 17:58:55 +00:00
Laurent Cozic
1023ec6206 Electron release v1.0.62 2018-02-12 17:56:42 +00:00
Laurent Cozic
7841421c0d All: Fixes #209: Items with non-ASCII characters end up truncated on Nextcloud 2018-02-12 18:15:22 +00:00
Laurent Cozic
995d8c35dd Updated readme 2018-02-11 20:49:28 +00:00
Laurent Cozic
b179471eff Electron: Fixes #205: Importing Evernote notes while on import page re-imports previous import 2018-02-11 20:31:26 +00:00
Laurent Cozic
19a126ebfe Android release v1.0.94 2018-02-11 17:29:09 +00:00
Laurent Cozic
7e56e5b587 Mobile: Fixes #207: Crash when changing dropdown value in config screen 2018-02-11 17:26:20 +00:00
Laurent Cozic
acf0c79341 Graduated E2EE and WebDAV from beta, and moved to v1.0 2018-02-11 13:50:59 +00:00
Laurent Cozic
9fe7e23ffe Android release v0.10.92 2018-02-11 13:20:32 +00:00
Laurent Cozic
c94cc93971 Fixed translator names 2018-02-10 13:03:01 +00:00
Laurent Cozic
b26094eba8 Fixed Basque flag 2018-02-10 12:52:57 +00:00
Laurent Cozic
89a5ccdf93 Added Basque translation, fixed issue with handling invalid translations. Updated translation FR. 2018-02-10 12:43:45 +00:00
Laurent Cozic
ce2da0e6dc Update readme 2018-02-09 16:03:43 +00:00
Laurent Cozic
f49d644b6a Electron release v0.10.61 2018-02-08 18:15:49 +00:00
Laurent Cozic
02ac0b8593 Removed uneeded created_time property 2018-02-07 20:42:52 +00:00
Laurent Cozic
78e5eaf1e2 Electron: Toolbar button to set tags 2018-02-07 20:35:11 +00:00
Laurent Cozic
fc0d227396 Electron: Allowing opening and saving resource images 2018-02-07 20:23:17 +00:00
Laurent Cozic
f91c52cdf7 Mobile: Update time when app is activated 2018-02-07 19:51:58 +00:00
Laurent Cozic
3f14878d0f All: Improved request repeating mechanism 2018-02-07 19:46:07 +00:00
Laurent Cozic
69fd32e7c6 All: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time 2018-02-07 19:02:07 +00:00
Laurent Cozic
80801cedf0 Android release v0.10.91 2018-02-07 17:57:08 +00:00
Laurent Cozic
480e4fa94b Merge branch 'master' of github.com:laurent22/joplin 2018-02-07 17:56:36 +00:00
Laurent Cozic
717c789836 Android release v0.10.90 2018-02-07 17:55:54 +00:00
Laurent Cozic
f099376446 Update README.md 2018-02-07 17:55:17 +00:00
Laurent Cozic
41fa9d093e Android release v0.10.89 2018-02-07 17:54:06 +00:00
Laurent Cozic
e2f3f81eb6 Android release v0.10.88 2018-02-07 17:51:31 +00:00
Laurent Cozic
5cab7aeb55 Fixed: Make sure alarms and resources are attached to right note when creating new note 2018-02-06 19:31:22 +00:00
Laurent Cozic
fa5f418c22 All: Added sync config check to config screens 2018-02-06 18:59:36 +00:00
Laurent Cozic
a25fcacace Electron: Display message when creating new note or to-do 2018-02-06 18:12:43 +00:00
Laurent Cozic
727ba7300e All: Also support $ as delimiter for Katex expressoins 2018-02-06 17:58:54 +00:00
Laurent Cozic
d25d9b3f44 CLI v0.10.93 2018-02-06 13:17:10 +00:00
Laurent Cozic
9d762a4319 Android release v0.10.86 2018-02-06 13:01:20 +00:00
Laurent Cozic
18d94c7585 Electron release v0.10.60 2018-02-06 12:57:40 +00:00
Laurent Cozic
af82345eb8 Fixed tray icon and update issue 2018-02-06 13:11:59 +00:00
Laurent Cozic
1e94a22986 Merge branch 'master' of github.com:laurent22/joplin 2018-02-06 13:06:59 +00:00
Laurent Cozic
e19a8a99ff macOS: Fixed startup crash 2018-02-06 13:05:36 +00:00
Laurent Cozic
f975009e24 Update readme 2018-02-05 19:02:11 +00:00
Laurent Cozic
90640fafc7 Merge branch 'master' of github.com:laurent22/joplin 2018-02-05 18:49:58 +00:00
Laurent Cozic
42e0e1e5a5 Updated build instructions 2018-02-06 09:42:20 +00:00
Laurent Cozic
61f64fa933 Added Markdown doc 2018-02-05 22:53:10 +00:00
Laurent Cozic
0d0ffd6d27 Added Markdown doc 2018-02-05 22:50:17 +00:00
Laurent Cozic
023ccffd2e Electron release v0.10.59 2018-02-05 22:20:31 +00:00
Laurent Cozic
bc26098c7d Android release v0.10.85 2018-02-05 22:19:21 +00:00
Laurent Cozic
7257a71a18 Mobile: Fixes #159: Make sure text fields aren't hidden by keyboard on iOS 2018-02-05 18:32:59 +00:00
Laurent Cozic
8ad8b73585 Better error handling for Katex and handling of code blocks and inline 2018-02-05 17:55:35 +00:00
Laurent Cozic
9a06815db9 Electron release v0.10.58 2018-02-05 17:34:39 +00:00
Laurent Cozic
66947d4954 Fixing appveyor script 2018-02-05 17:34:31 +00:00
Laurent Cozic
3ec22185d5 Electron release v0.10.57 2018-02-05 17:32:54 +00:00
Laurent Cozic
0f05c23e26 Fixing deployment script 2018-02-05 17:32:48 +00:00
Laurent Cozic
74493fece0 Android release v0.10.83 2018-02-05 17:32:00 +00:00
Laurent Cozic
557a96e814 Electron release v0.10.56 2018-02-05 17:27:38 +00:00
Laurent Cozic
4b23b419a4 Electron release v0.10.55 2018-02-05 17:27:09 +00:00
Laurent Cozic
8b7f5b1151 Fix scrolling issue in Electron app 2018-02-04 18:45:52 +00:00
Laurent Cozic
29e9ccf216 Electron: Reverted to older Sharp version to fix Ubuntu issue 2018-02-04 18:31:13 +00:00
Laurent Cozic
2c04f5c8bc Improved Android and Electron release process 2018-02-04 18:05:07 +00:00
Laurent Cozic
5430a747e9 Improved Android and Electron release process 2018-02-04 17:55:22 +00:00
Laurent Cozic
13bc185829 Improved Android and Electron release process 2018-02-04 17:51:42 +00:00
Laurent Cozic
ed87581a8a Improved Android and Electron release process 2018-02-04 17:48:29 +00:00
Laurent Cozic
2645ec96a8 Fixed tool utils 2018-02-04 17:44:32 +00:00
Laurent Cozic
d278d830f0 Improved Android and Electron release process 2018-02-04 17:42:33 +00:00
Laurent Cozic
b4dce0ed46 All: Added Katex support 2018-02-04 17:12:24 +00:00
Laurent Cozic
e8416042d4 Merge branch 'master' into math-support 2018-02-02 20:35:32 +00:00
Laurent Cozic
70adbe5e76 Added flags 2018-02-01 20:21:54 +00:00
Laurent Cozic
f66be08d1d Added list of translated languages to README file 2018-02-01 20:15:31 +00:00
Laurent Cozic
fad96f5266 All: Added section to list missing master keys 2018-02-01 19:01:20 +00:00
Laurent Cozic
c33a7f5f47 Updated French translation 2018-02-02 00:08:37 +00:00
Laurent Cozic
28afbcde02 Updated translations 2018-02-02 00:02:47 +00:00
Laurent Cozic
691292d2b3 Merge branch 'master' of github.com:laurent22/joplin 2018-02-01 23:40:15 +00:00
Laurent Cozic
30ff81064f All: Made WebDAV driver more generic to support services other than Nextcloud and added WebDAV sync target 2018-02-01 23:40:05 +00:00
Laurent Cozic
f9f398ad98 Merge pull request #194 from rtmkrlv/russian-locale
Update Russian translation
2018-02-01 15:26:15 +01:00
rtmkrlv
537884bdcd Merge branch 'master' into russian-locale 2018-02-01 15:50:39 +02:00
rtmkrlv
d54400a7cb Updated Russian translation 2018-02-01 15:43:26 +02:00
Laurent Cozic
42c78264fb Update website 2018-01-31 20:21:38 +00:00
Laurent Cozic
c52da82447 Electron: Fix: Don't allow adding notes and to-do to conflict notebook 2018-01-31 20:19:11 +00:00
Laurent Cozic
cca43624e4 Electron: Added tray icon support 2018-01-31 20:10:32 +00:00
Laurent Cozic
dac1cd7668 Mobile: Allow filtering log by warning/error 2018-01-31 19:51:29 +00:00
Laurent Cozic
b4c00db0e3 Electron release v0.10.54 2018-01-31 19:40:40 +00:00
Laurent Cozic
3ce393a8b2 Electron release v0.10.53 2018-01-31 19:34:47 +00:00
Laurent Cozic
2b627fe4ab Fixed auto-update check 2018-01-31 19:34:38 +00:00
Laurent Cozic
fcf8a1649d CLI v0.10.92 2018-01-31 19:28:31 +00:00
Laurent Cozic
8d3b050831 Update website 2018-01-31 19:26:21 +00:00
Laurent Cozic
43297ef0a3 Updated translations 2018-01-31 19:14:32 +00:00
Laurent Cozic
551fabdfc9 Tweak and error handling on auto-update 2018-01-31 19:10:45 +00:00
Laurent Cozic
d6de56b2db All: Fixed crash when having invalid UTF-8 string in text editor 2018-01-31 19:01:11 +00:00
Laurent Cozic
9e979804f3 Electron release v0.10.52 2018-01-31 17:53:18 +00:00
Laurent Cozic
b8e0f182cc Android release v0.10.81 2018-01-31 17:51:22 +00:00
Laurent Cozic
9a41b9e192 Electron: Improved auto-update process to avoid random crashes 2018-01-30 22:35:50 +00:00
Laurent Cozic
9b8f520b9f Electron: Allow focusing either title or body when creating a new note or to-odo 2018-01-30 21:49:22 +00:00
Laurent Cozic
5b6019805c Electron: Fixed auto-title when title is manually entered first 2018-01-30 21:36:54 +00:00
Laurent Cozic
a4106436c4 Fixed delta function when processing many items 2018-01-30 21:10:54 +00:00
Laurent Cozic
f6b4eb511e Add Content-Size header for WebDAV, which is required by some services 2018-01-30 20:24:09 +00:00
Laurent Cozic
eb67ac17a0 Allow decryption to continue even if an item cannot be decrypted 2018-01-30 20:15:05 +00:00
Laurent Cozic
7b760d03ef All: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only) 2018-01-30 20:10:36 +00:00
Laurent Cozic
2805ae2acf Fixed crash when calling fetch() with invalid URL in RN app 2018-01-30 19:01:07 +00:00
Laurent Cozic
5cb5ccc781 All: Optimised Nextcloud sync delta functionality 2018-01-29 20:51:14 +00:00
Laurent Cozic
0dba2821b6 Merge pull request #189 from strobeltobias/patch-2
Update German translation
2018-01-29 18:59:20 +00:00
Tobias Strobel
1db7825b22 Update German translation
Grammar and wording fixes, translating new strings.
2018-01-29 14:24:50 +01:00
Laurent Cozic
8a92d6ad70 Update website 2018-01-28 18:47:22 +00:00
Laurent Cozic
138ad9fcad Mobile: Fixes #114: Update geolocation in metadata after it has been set 2018-01-28 18:42:43 +00:00
Laurent Cozic
08cb518c25 Check if current folder exists 2018-01-28 18:19:56 +00:00
Laurent Cozic
6d04eab200 Merge pull request #185 from pf-siedler/autocomp_file_and_item
Autocompletion for files and items
2018-01-28 18:17:01 +00:00
Laurent Cozic
8a8cb51e1b Electron release v0.10.51 2018-01-28 18:07:08 +00:00
Laurent Cozic
5c66042a2d CLI v0.10.91 2018-01-28 17:59:34 +00:00
Laurent Cozic
ae75181b02 Electron release v0.10.50 2018-01-28 17:52:15 +00:00
Laurent Cozic
9dc3238182 Added beta notice 2018-01-28 17:51:38 +00:00
Laurent Cozic
0a68749373 Android release v0.10.79 2018-01-28 17:45:54 +00:00
Laurent Cozic
1519116291 Updated French translation 2018-01-28 17:43:21 +00:00
Laurent Cozic
d023d841e2 Update translations 2018-01-28 17:38:30 +00:00
Laurent Cozic
d7a1465d8e Skip sync report events in log 2018-01-28 17:38:17 +00:00
Laurent Cozic
15848fc696 Closing a resource is async 2018-01-28 17:37:51 +00:00
Laurent Cozic
837ae2c9f2 Send only one NOT_LOADED event per master key 2018-01-28 17:37:29 +00:00
Laurent Cozic
6789b98ead Return fileNotFound error when file cannot be opened so that it is skipped by synchroniser 2018-01-28 17:37:03 +00:00
Laurent Cozic
29f6e74ee3 Convert fs errors to normal errors 2018-01-28 17:36:36 +00:00
Laurent Cozic
2780c38c45 Fixed WebDAV error handling 2018-01-28 17:36:11 +00:00
Laurent Cozic
4531838217 Display stars for secure config value 2018-01-28 17:35:20 +00:00
pf-siedler
7bccf7f65d Implement auto completaion for <item> usage 2018-01-28 23:12:54 +09:00
pf-siedler
c62a24a9cb Implement auto completion for <file> usage
Suggest file names from current directory.
2018-01-28 22:59:58 +09:00
Laurent Cozic
c6830499f7 Fixed Travis 2018-01-28 11:48:29 +00:00
Laurent Cozic
d9f00a2539 Electron release v0.10.49 2018-01-26 17:21:34 +00:00
Laurent Cozic
def83c9119 Merge branch 'master' of github.com:laurent22/joplin 2018-01-26 17:19:58 +00:00
Laurent Cozic
b6cb0056c7 CLI v0.10.90 2018-01-26 17:19:42 +00:00
Laurent Cozic
1669b5258a Fixed detection of encrypted item 2018-01-25 23:01:18 +00:00
Laurent Cozic
5a9e0bfc26 Handle password text input in mobile and desktop 2018-01-25 22:44:09 +00:00
Laurent Cozic
8f3fdb3afe Tweaks to make sure Nextcloud driver passes all test units 2018-01-25 21:15:58 +00:00
Laurent Cozic
7ab135c099 Various tweaks to get Nextcloud working in mobile 2018-01-25 20:48:01 +00:00
Laurent Cozic
1cc27f2509 Got Nextcloud sync to work in Electron 2018-01-25 19:01:14 +00:00
Laurent Cozic
ef700b421c Update CONTRIBUTING.md 2018-01-25 13:35:04 +00:00
Laurent Cozic
b9af5ac052 Update issue_template.md 2018-01-25 13:34:07 +00:00
Laurent Cozic
173f2d421d Update PULL_REQUEST_TEMPLATE 2018-01-25 13:32:19 +00:00
Laurent Cozic
9f82c069c9 Update CONTRIBUTING.md 2018-01-25 13:31:17 +00:00
Laurent Cozic
6ade09c228 Merge branch 'master' into webdav 2018-01-24 17:25:34 +00:00
Laurent Cozic
5393a1399c Finished WebDAV driver 2018-01-23 20:10:20 +00:00
Laurent Cozic
fd29f20b2e Electron: Fix checkbox issue in config screen 2018-01-23 18:31:49 +00:00
Laurent Cozic
c011b53d1f Electron: Upgraded Electron to 1.7.11 to fix security vulnerability 2018-01-23 18:10:30 +00:00
Laurent Cozic
26e3a7b68c Merge branch 'master' of github.com:laurent22/joplin 2018-01-23 17:53:19 +00:00
Laurent Cozic
e70a291698 Merge pull request #174 from gabcoh/fix171
Fix #171
2018-01-23 11:26:11 +00:00
Laurent Cozic
511bd57726 Merge pull request #175 from alexdevero/add-font-size-settings
Add font size settings
2018-01-23 11:25:47 +00:00
Laurent Cozic
c6de8598dc Electron release v0.10.48 2018-01-22 19:10:29 +00:00
Laurent Cozic
7bee25599d Removed uneeded code 2018-01-22 19:06:50 +00:00
Laurent Cozic
773a1ad829 Travis: only build tags 2018-01-21 20:03:40 +00:00
Laurent Cozic
1a1e264fa4 All: Refactored so that memory and file sync target use same delta logic 2018-01-21 19:45:32 +00:00
Laurent Cozic
5b99ecefca Merge branch 'master' into webdav 2018-01-21 19:10:39 +00:00
Laurent Cozic
1bfeed377a All: Optimised file sync logic so that it doesn't fetch the content of
all the items on each sync. Also limit the number of items in a batch
to 1000
2018-01-21 18:54:47 +00:00
Laurent Cozic
86eee376bb All: Handle case where resource blob is missing during sync 2018-01-21 17:48:50 +00:00
Laurent Cozic
6a7d368184 All: Started Nextcloud support 2018-01-21 17:01:37 +00:00
Alex Devero
1da19ae98d Fix indentation 2018-01-19 14:11:40 +01:00
Alex Devero
f52c117b09 Add font size settings 2018-01-19 13:27:44 +01:00
Laurent Cozic
2551f96149 Fixed Readme 2018-01-18 22:43:37 +00:00
Laurent Cozic
c984c19fee Android release v0.10.78 2018-01-18 22:35:05 +00:00
Laurent Cozic
ac8e91e82e Started FAQ 2018-01-18 22:34:27 +00:00
Gabe Cohen
af50d80541 Fix #171 2018-01-18 14:29:13 -06:00
Laurent Cozic
e355f4e49b Fixed license 2018-01-18 20:14:05 +00:00
Laurent Cozic
53da63e371 Trying to add math support 2018-01-11 19:51:01 +00:00
150 changed files with 8597 additions and 2308 deletions

5
.gitignore vendored
View File

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

View File

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

View File

@@ -17,6 +17,15 @@ If you get a node-gyp related error you might need to manually install it: `npm
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
Before building any of the applications, you need to build the tools:
```
cd Tools
npm install
```
# Building the Electron application
```

View File

@@ -1,6 +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.
# Style
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

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

View File

@@ -312,6 +312,63 @@ class Application extends BaseApplication {
return this.activeCommand_;
}
async loadKeymaps() {
const defaultKeyMap = [
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
];
// Filter the keymap item by command so that items in keymap.json can override
// the default ones.
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
}
const filePath = Setting.value('profileDir') + '/keymap.json';
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
const keymap = JSON.parse(configString);
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
const item = keymap[keymapIndex];
itemsByCommand[item.command] = item;
}
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load keymap ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
}
const output = [];
for (let n in itemsByCommand) {
if (!itemsByCommand.hasOwnProperty(n)) continue;
output.push(itemsByCommand[n]);
}
return output;
}
async start(argv) {
argv = await super.start(argv);
@@ -338,8 +395,10 @@ class Application extends BaseApplication {
} else { // Otherwise open the GUI
this.initRedux();
const keymap = await this.loadKeymaps();
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store());
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
await this.gui_.start();

View File

@@ -4,6 +4,7 @@ var Folder = require('lib/models/Folder.js');
var Tag = require('lib/models/Tag.js');
var { cliUtils } = require('./cli-utils.js');
var yargParser = require('yargs-parser');
var fs = require('fs-extra');
async function handleAutocompletionPromise(line) {
// Auto-complete the command name
@@ -48,7 +49,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) {
@@ -71,8 +72,10 @@ async function handleAutocompletionPromise(line) {
let argName = cmdUsage[positionalArgs - 1];
argName = cliUtils.parseCommandArg(argName).name;
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
l.push(...notes.map((n) => n.title));
}
@@ -81,11 +84,22 @@ async function handleAutocompletionPromise(line) {
l.push(...folders.map((n) => n.title));
}
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: next + '*' });
l.push(...tags.map((n) => n.title));
}
if (argName == 'file') {
let files = await fs.readdir('.');
l.push(...files);
}
if (argName == 'tag-command') {
let c = filterList(['add', 'remove', 'list'], next);
l.push(...c);

View File

@@ -23,8 +23,11 @@ class Command extends BaseCommand {
const verbose = args.options.verbose;
const renderKeyValue = (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

@@ -131,7 +131,6 @@ class Command extends BaseCommand {
} else if (stat.isDirectory()) {
continue;
} else {
itemCount++;
const content = await fs.readFile(fullPath, 'utf8');
const item = await BaseItem.unserialize(content);
const ItemClass = BaseItem.itemClass(item);
@@ -141,6 +140,8 @@ class Command extends BaseCommand {
continue;
}
itemCount++;
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
if (isEncrypted) {

View File

@@ -36,21 +36,22 @@ class Command extends BaseCommand {
async action(args) {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts') {
if (args.command === 'shortcuts' || args.command === 'keymap') {
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'http://joplin.cozic.net/terminal/#shortcuts'));
this.stdout('');
if (app().gui().isDummy()) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const shortcuts = app().gui().shortcuts();
const keymap = app().gui().keymap();
let rows = [];
for (let n in shortcuts) {
if (!shortcuts.hasOwnProperty(n)) continue;
const shortcut = shortcuts[n];
if (!shortcut.description) continue;
n = shortcut.friendlyName ? shortcut.friendlyName : n;
rows.push([n, shortcut.description()]);
for (let i = 0; i < keymap.length; i++) {
const item = keymap[i];
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
rows.push([keys.join(', '), item.command]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
@@ -78,7 +79,7 @@ class Command extends BaseCommand {
this.stdout(_('To maximise/minimise the console, press "TC".'));
this.stdout(_('To enter command line mode, press ":"'));
this.stdout(_('To exit command line mode, press ESCAPE'));
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`'));
}
app().gui().showConsole();

View File

@@ -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() {
@@ -86,7 +101,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir'));
const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
try {
@@ -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

@@ -66,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

@@ -7,39 +7,15 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Samuel Blickle <blickle.samuel@gmail.com>\n"
"Last-Translator: Tobias Strobel <git@strobeltobias.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
msgstr "Das nächste Fenster fokussieren"
msgid "Give focus to previous pane"
msgstr "Das vorherige Fenster fokussieren"
msgid "Enter command line mode"
msgstr "Zum Terminal-Modus wechseln"
msgid "Exit command line mode"
msgstr "Den Terminal-Modus verlassen"
msgid "Edit the selected note"
msgstr "Die ausgewählte Notiz bearbeiten"
msgid "Cancel the current command."
msgstr "Den momentanen Befehl abbrechen."
msgid "Exit the application."
msgstr "Das Programm verlassen."
msgid "Delete the currently selected note or notebook."
msgstr "Die/das momentan ausgewählte Notiz(-buch) löschen."
msgid "To delete a tag, untag the associated notes."
msgstr ""
"Hebe die Markierungen zugehöriger Notizen auf, um eine Markierung zu löschen."
@@ -49,35 +25,6 @@ msgstr ""
"Wähle bitte zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden "
"soll."
msgid "Set a to-do as completed / not completed"
msgstr "Ein To-Do as abgeschlossen / nicht abgeschlossen markieren"
#, fuzzy
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
"Schal[t]e das Terminal zwischen maximiert/minimiert/versteckt/sichtbar um."
msgid "Search"
msgstr "Suchen"
msgid "[t]oggle note [m]etadata."
msgstr "Notiz-[M]etadata einschal[t]en."
msgid "[M]ake a new [n]ote"
msgstr "Eine neue [N]otiz [m]achen"
msgid "[M]ake a new [t]odo"
msgstr "Ein neues [T]o-Do [m]achen"
msgid "[M]ake a new note[b]ook"
msgstr "Ein neues Notiz[b]uch [m]achen"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Die Notiz zu einem Notizbuch kopieren."
msgid "Move the note to a notebook."
msgstr "Die Notiz zu einem Notizbuch verschieben."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Drücke Strg+D oder tippe \"exit\", um das Programm zu verlassen"
@@ -117,7 +64,7 @@ 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 ""
msgstr "Kann verschlüsseltes Objekt nicht ändern"
#, javascript-format
msgid "Missing required argument: %s"
@@ -187,31 +134,34 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Verwaltet die E2EE-Konfiguration. Die Befehle sind `enable`, `disable`, "
"`decrypt`, `status` und `target-status`."
msgid "Enter master password:"
msgstr ""
msgstr "Master-Passwort eingeben:"
msgid "Operation cancelled"
msgstr ""
msgstr "Vorgang abgebrochen"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Entschlüsselung starten.... Warte bitte, da es einige Minuten dauern kann, "
"je nachdem, wie viel es zu entschlüsseln gibt."
msgid "Completed decryption."
msgstr ""
msgstr "Entschlüsselung abgeschlossen."
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgstr "Aktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgstr "Die Verschlüsselung ist: %s"
msgid "Edit note."
msgstr "Notiz bearbeiten."
@@ -234,6 +184,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 "Fehler beim Öffnen der Notiz im Editor: %s"
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@@ -260,16 +214,19 @@ msgstr "Zeigt die Standort-URL der Notiz an."
msgid "Displays usage information."
msgstr "Zeigt die Nutzungsstatistik an."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
#, fuzzy
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
"Tippe `help [Befehl]` ein, um mehr Informationen über einen Befehl zu "
"erhalten."
"Tippe `help [Befehl]` für weitere Informationen über einen Befehl; oder "
"tippe `help all` für die vollständigen Informationen zur Befehlsverwendung."
msgid "The possible commands are:"
msgstr "Mögliche Befehle sind:"
@@ -305,8 +262,9 @@ msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Um den Kommandozeilen Modus zu beenden, drücke ESCAPE"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Um die komplette Liste von verfügbaren Tastenkürzeln anzuzeigen, tippe `help "
"shortcuts` ein"
@@ -470,8 +428,19 @@ 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 ""
"Keine Authentifizierung mit %s. Gib bitte alle fehlenden Zugangsdaten an."
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
msgstr "Synchronisation wird bereits ausgeführt."
#, javascript-format
msgid ""
@@ -483,12 +452,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)"
@@ -500,9 +463,8 @@ msgid "Starting synchronisation..."
msgstr "Starte Synchronisation..."
msgid "Cancelling... Please wait."
msgstr "Breche ab... Bitte warten."
msgstr "Abbrechen... Bitte warten."
#, fuzzy
msgid ""
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
"[tag] from [note], or to list the notes associated with [tag]. The command "
@@ -511,7 +473,7 @@ msgstr ""
"<tag-command> kann \"add\", \"remove\" or \"list\" sein, um eine "
"[Markierung] zu [Notiz] zuzuweisen oder zu entfernen, oder um mit "
"[Markierung] markierte Notizen anzuzeigen. Mit dem Befehl `tag list` können "
"alle Notizen angezeigt werden."
"alle Markierungen angezeigt werden."
#, javascript-format
msgid "Invalid command: \"%s\""
@@ -546,7 +508,7 @@ msgid "%s %s (%s)"
msgstr "%s %s (%s)"
msgid "Enum"
msgstr ""
msgstr "Aufzählung"
#, javascript-format
msgid "Type: %s."
@@ -612,6 +574,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Ein oder mehrere Objekte sind derzeit verschlüsselt und es kann erforderlich "
"sein, ein Master-Passwort zu hinterlegen. Gib dazu bitte `e2ee decrypt` ein. "
"Wenn du das Passwort bereits eingegeben hast, werden die verschlüsselten "
"Objekte im Hintergrund entschlüsselt und stehen in Kürze zur Verfügung."
msgid "File"
msgstr "Datei"
@@ -631,6 +597,10 @@ msgstr "Evernote Notizen importieren"
msgid "Evernote Export Files"
msgstr "Evernote Export Dateien"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Verlassen"
@@ -649,6 +619,12 @@ msgstr "Einfügen"
msgid "Search in all the notes"
msgstr "Alle Notizen durchsuchen"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Werkzeuge"
@@ -656,11 +632,10 @@ msgid "Synchronisation status"
msgstr "Status der Synchronisation"
msgid "Encryption options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
#, fuzzy
msgid "General Options"
msgstr "Optionen"
msgstr "Allgemeine Einstellungen"
msgid "Help"
msgstr "Hilfe"
@@ -668,6 +643,9 @@ msgstr "Hilfe"
msgid "Website and documentation"
msgstr "Webseite und Dokumentation"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Über Joplin"
@@ -675,12 +653,43 @@ msgstr "Über Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Auf %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Abbrechen"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Notizen löschen?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen gespeichert in: %s"
@@ -693,6 +702,9 @@ msgid ""
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Durch die Deaktivierung der Verschlüsselung werden *alle* Notizen und "
"Anhänge neu synchronisiert und unverschlüsselt an das Synchronisierungsziel "
"gesendet. Möchtest du fortfahren?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@@ -700,15 +712,20 @@ 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 ""
"Durch das Aktivieren der Verschlüsselung werden alle Notizen und Anhänge neu "
"synchronisiert und verschlüsselt an das Synchronisationsziel gesendet. Achte "
"darauf, dass du das Passwort nicht verlierst, da dies aus Sicherheitsgründen "
"die einzige Möglichkeit ist, deine Daten zu entschlüsseln! Um die "
"Verschlüsselung zu aktivieren, gib bitte unten dein Passwort ein."
msgid "Disable encryption"
msgstr ""
msgstr "Verschlüsselung deaktivieren"
msgid "Enable encryption"
msgstr ""
msgstr "Verschlüsselung aktivieren"
msgid "Master Keys"
msgstr ""
msgstr "Hauptschlüssel"
msgid "Active"
msgstr "Aktiv"
@@ -741,11 +758,21 @@ msgstr ""
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
"Notizbücher ursprünglich verschlüsselt wurden."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hauptschlüssel"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
msgid "Encryption is:"
msgstr ""
msgstr "Die Verschlüsselung ist:"
msgid "Back"
msgstr "Zurück"
@@ -778,6 +805,9 @@ msgstr "Benne Notizbuch um:"
msgid "Set alarm:"
msgstr "Alarm erstellen:"
msgid "Search"
msgstr "Suchen"
msgid "Layout"
msgstr "Layout"
@@ -787,12 +817,11 @@ msgstr "Manche Objekte können nicht synchronisiert werden."
msgid "View them now"
msgstr "Zeige sie jetzt an"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Kann Synchronisierer nicht initialisieren."
msgstr "Einige Objekte können nicht entschlüsselt werden."
msgid "Set the password"
msgstr ""
msgstr "Setze ein Passwort"
msgid "Add or remove tags"
msgstr "Markierungen hinzufügen oder entfernen"
@@ -817,6 +846,13 @@ msgstr ""
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den "
"(+) Knopf drückst."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Änderungen speichern"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Nicht unterstützter Link oder Nachricht: %s"
@@ -824,14 +860,29 @@ msgstr "Nicht unterstützter Link oder Nachricht: %s"
msgid "Attach file"
msgstr "Datei anhängen"
msgid "Tags"
msgstr "Markierungen"
msgid "Set alarm"
msgstr "Alarm erstellen"
#, fuzzy
msgid "to-do"
msgstr "Neues To-Do"
#, fuzzy
msgid "note"
msgstr "Neue Notiz"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Importiere Notizen..."
msgid "Refresh"
msgstr "Aktualisieren"
msgid "Clear"
msgstr ""
msgstr "Leeren"
msgid "OneDrive Login"
msgstr "OneDrive Login"
@@ -846,7 +897,7 @@ msgid "Synchronisation Status"
msgstr "Synchronisations Status"
msgid "Encryption Options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
msgid "Remove this tag from all the notes?"
msgstr "Diese Markierung von allen Notizen entfernen?"
@@ -863,9 +914,6 @@ msgstr "Synchronisieren"
msgid "Notebooks"
msgstr "Notizbücher"
msgid "Tags"
msgstr "Markierungen"
msgid "Searches"
msgstr "Suchen"
@@ -884,12 +932,20 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system"
msgstr "Dateisystem"
#, fuzzy
msgid "Nextcloud"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Nur für Tests)"
#, fuzzy
msgid "WebDAV"
msgstr "Nexcloud WebDAV URL"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Unbekanntes Log Level: %s"
@@ -948,16 +1004,16 @@ msgstr "Lokale Objekte gelöscht: %d."
msgid "Deleted remote items: %d."
msgstr "Remote Objekte gelöscht: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Lokale Objekte erstellt: %d."
msgstr "Geladene Objekte: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Status: \"%s\"."
msgid "Cancelling..."
msgstr "Breche ab..."
msgstr "Abbrechen..."
#, javascript-format
msgid "Completed: %s"
@@ -968,11 +1024,10 @@ msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid "Encrypted"
msgstr ""
msgstr "Verschlüsselt"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Manche Objekte können nicht synchronisiert werden."
msgstr "Verschlüsselte Objekte können nicht verändert werden."
msgid "Conflicts"
msgstr "Konflikte"
@@ -1029,12 +1084,37 @@ msgstr "Hell"
msgid "Dark"
msgstr "Dunkel"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Erstellt ein neues To-Do."
#, fuzzy
msgid "Focus title"
msgstr "Notiz Titel:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Erstellt eine neue Notiz."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Einstellen des Anwendungszooms"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Synchronisation interval"
msgstr "Synchronisationsinterval"
@@ -1050,9 +1130,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"
@@ -1060,12 +1137,12 @@ 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."
"Das Ziel, mit dem synchronisiert werden soll. Jedes Synchronisationsziel "
"kann zusätzliche Parameter haben, die als `sync.NUM.NAME` (alle unten "
"dokumentiert) bezeichnet werden."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@@ -1074,8 +1151,29 @@ msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
"Der Pfad, mit dem synchronisiert werden soll, wenn die Dateisystem-"
"Synchronisation aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Nexcloud Benutzername"
msgid "Nexcloud password"
msgstr "Nexcloud Passwort"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Nexcloud Benutzername"
#, fuzzy
msgid "WebDAV password"
msgstr "Setze ein Passwort"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@@ -1084,15 +1182,18 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%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 ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)"
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
@@ -1136,13 +1237,13 @@ msgid "Delete these notes?"
msgstr "Sollen diese Notizen gelöscht werden?"
msgid "Log"
msgstr "Log"
msgstr "Protokoll"
msgid "Export Debug Report"
msgstr "Fehlerbreicht exportieren"
msgstr "Fehlerbericht exportieren"
msgid "Encryption Config"
msgstr ""
msgstr "Verschlüsselungskonfiguration"
msgid "Configuration"
msgstr "Konfiguration"
@@ -1155,7 +1256,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Tippe hier, um das Entschlüsselungspasswort festzulegen."
msgid "Select date"
msgstr "Datum auswählen"
@@ -1168,22 +1269,20 @@ msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Master Key %s"
msgstr ""
msgstr "Hauptschlüssel %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Erstellt: %d."
msgstr "Erstellt: %s"
#, fuzzy
msgid "Password:"
msgstr "Passwort"
msgstr "Passwort:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Passwort darf nicht leer sein"
#, fuzzy
msgid "Enable"
msgstr "Deaktiviert"
msgstr "Aktivieren"
#, javascript-format
msgid "The notebook could not be saved: %s"
@@ -1192,6 +1291,12 @@ msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
msgid "Edit notebook"
msgstr "Notizbuch bearbeiten"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Diese Notiz wurde verändert:"
@@ -1248,8 +1353,63 @@ msgstr ""
msgid "Welcome"
msgstr "Willkommen"
#~ msgid "Note title:"
#~ msgstr "Notizen Titel:"
#~ msgid "Give focus to next pane"
#~ msgstr "Das nächste Fenster fokussieren"
#~ msgid "Give focus to previous pane"
#~ msgstr "Das vorherige Fenster fokussieren"
#~ msgid "Enter command line mode"
#~ msgstr "Zum Terminal-Modus wechseln"
#~ msgid "Exit command line mode"
#~ msgstr "Den Terminal-Modus verlassen"
#~ msgid "Edit the selected note"
#~ msgstr "Die ausgewählte Notiz bearbeiten"
#~ msgid "Cancel the current command."
#~ msgstr "Den momentanen Befehl abbrechen."
#~ msgid "Exit the application."
#~ msgstr "Das Programm verlassen."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Die/das momentan ausgewählte Notiz(-buch) löschen."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Ein To-Do als abgeschlossen / nicht abgeschlossen markieren"
#, fuzzy
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "Schal[t]e das Terminal zwischen maximiert/minimiert/versteckt/sichtbar um."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "Notiz-[M]etadata einschal[t]en."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "Eine neue [N]otiz [m]achen"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "Ein neues [T]o-Do [m]achen"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "Ein neues Notiz[b]uch [m]achen"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Die Notiz zu einem Notizbuch kopieren."
#~ msgid "Move the note to a notebook."
#~ msgstr "Die Notiz zu einem Notizbuch verschieben."
#~ 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 "To-do title:"
#~ msgstr "To-Do Titel:"

View File

@@ -15,63 +15,12 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Give focus to next pane"
msgstr ""
msgid "Give focus to previous pane"
msgstr ""
msgid "Enter command line mode"
msgstr ""
msgid "Exit command line mode"
msgstr ""
msgid "Edit the selected note"
msgstr ""
msgid "Cancel the current command."
msgstr ""
msgid "Exit the application."
msgstr ""
msgid "Delete the currently selected note or notebook."
msgstr ""
msgid "To delete a tag, untag the associated notes."
msgstr ""
msgid "Please select the note or notebook to be deleted first."
msgstr ""
msgid "Set a to-do as completed / not completed"
msgstr ""
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
msgid "Search"
msgstr ""
msgid "[t]oggle note [m]etadata."
msgstr ""
msgid "[M]ake a new [n]ote"
msgstr ""
msgid "[M]ake a new [t]odo"
msgstr ""
msgid "[M]ake a new note[b]ook"
msgstr ""
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr ""
msgid "Move the note to a notebook."
msgstr ""
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr ""
@@ -214,6 +163,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -237,6 +190,10 @@ msgstr ""
msgid "Displays usage information."
msgstr ""
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr ""
@@ -272,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
msgstr ""
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
msgid "Imports an Evernote notebook file (.enex file)."
@@ -413,6 +370,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -423,10 +388,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -545,6 +506,10 @@ msgstr ""
msgid "Evernote Export Files"
msgstr ""
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr ""
@@ -563,6 +528,12 @@ msgstr ""
msgid "Search in all the notes"
msgstr ""
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr ""
@@ -581,6 +552,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -588,12 +562,41 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -650,6 +653,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@@ -685,6 +697,9 @@ msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Search"
msgstr ""
msgid "Layout"
msgstr ""
@@ -719,6 +734,12 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
msgid "Open..."
msgstr ""
msgid "Save as..."
msgstr ""
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr ""
@@ -726,9 +747,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "to-do"
msgstr ""
msgid "note"
msgstr ""
#, javascript-format
msgid "Creating new %s..."
msgstr ""
msgid "Refresh"
msgstr ""
@@ -765,9 +799,6 @@ msgstr ""
msgid "Notebooks"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Searches"
msgstr ""
@@ -785,12 +816,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -917,12 +954,33 @@ msgstr ""
msgid "Dark"
msgstr ""
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -938,9 +996,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +1003,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)"
@@ -960,6 +1015,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1071,6 +1144,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -16,64 +16,12 @@ msgstr ""
"X-Generator: Poedit 2.0.4\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
msgstr "Dar enfoque al siguiente panel"
msgid "Give focus to previous pane"
msgstr "Dar enfoque al panel anterior"
msgid "Enter command line mode"
msgstr "Entrar modo linea de comandos"
msgid "Exit command line mode"
msgstr "Salir modo linea de comandos"
msgid "Edit the selected note"
msgstr "Editar la nota seleccionada"
msgid "Cancel the current command."
msgstr "Cancelar el comando actual."
msgid "Exit the application."
msgstr "Salir de la aplicación."
msgid "Delete the currently selected note or notebook."
msgstr "Eliminar la nota o libreta seleccionada."
msgid "To delete a tag, untag the associated notes."
msgstr "Para eliminar una etiqueta, desmarca las notas asociadas."
msgid "Please select the note or notebook to be deleted first."
msgstr "Por favor selecciona la nota o libreta a elliminar."
#, fuzzy
msgid "Set a to-do as completed / not completed"
msgstr "Marca una tarea como completado / no completado"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "[c]ambia la [c]onsola entre maximizado/minimizado/oculto/visible."
msgid "Search"
msgstr "Buscar"
msgid "[t]oggle note [m]etadata."
msgstr "[c]ambia los [m]etadatos de una nota."
msgid "[M]ake a new [n]ote"
msgstr "[H]acer una [n]ota nueva"
msgid "[M]ake a new [t]odo"
msgstr "[H]acer una nueva [t]area"
msgid "[M]ake a new note[b]ook"
msgstr "[H]acer una nueva [l]ibreta"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copiar ([Y]ank) la [n]ota a una libreta."
msgid "Move the note to a notebook."
msgstr "Mover la nota a una libreta."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Presiona Ctrl+D o escribe \"salir\" para salir de la aplicación"
@@ -224,6 +172,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."
@@ -249,6 +201,10 @@ msgstr "Mostrar geolocalización de la URL para la nota."
msgid "Displays usage information."
msgstr "Muestra información de uso."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Atajos no disponibles en modo CLI."
@@ -290,8 +246,9 @@ msgstr "Para entrar a modo linea de comando, presiona \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Para salir de modo linea de comando, presiona ESCAPE"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Para una lista completa de los atajos de teclado disponibles, escribe `help "
"shortcuts`"
@@ -449,6 +406,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."
@@ -462,10 +427,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)"
@@ -601,6 +562,10 @@ msgstr "Importar notas de Evernote"
msgid "Evernote Export Files"
msgstr "Exportar archivos de Evernote"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Salir"
@@ -619,6 +584,12 @@ msgstr "Pegar"
msgid "Search in all the notes"
msgstr "Buscar en todas las notas"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Herramientas"
@@ -639,6 +610,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentacion"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -646,12 +620,43 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "Ok"
msgid "Cancel"
msgstr "Cancelar"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Eliminar notas?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Sincronizacion cancelada"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -710,6 +715,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Estatus"
@@ -747,6 +761,9 @@ msgstr "Renombrar libreta:"
msgid "Set alarm:"
msgstr "Ajustar alarma:"
msgid "Search"
msgstr "Buscar"
msgid "Layout"
msgstr "Diseño"
@@ -786,6 +803,13 @@ msgid ""
msgstr ""
"Actualmente no hay notas. Crea una nueva nota dando client en el boton (+)."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Guardar cambios"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Enlace o mensaje sin soporte: %s"
@@ -793,9 +817,24 @@ msgstr "Enlace o mensaje sin soporte: %s"
msgid "Attach file"
msgstr "Adjuntar archivo"
msgid "Tags"
msgstr "Etiquetas"
msgid "Set alarm"
msgstr "Ajustar alarma"
#, fuzzy
msgid "to-do"
msgstr "Nueva lista de tareas"
#, fuzzy
msgid "note"
msgstr "Nueva nota"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Importando notas..."
msgid "Refresh"
msgstr "Refrescar"
@@ -833,9 +872,6 @@ msgstr "Sincronizar"
msgid "Notebooks"
msgstr "Libretas"
msgid "Tags"
msgstr "Etiquetas"
msgid "Searches"
msgstr "Busquedas"
@@ -854,6 +890,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -861,6 +900,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev(Solo para pruebas)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -1003,12 +1045,36 @@ msgid "Dark"
msgstr "Oscuro"
#, fuzzy
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
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"
@@ -1024,22 +1090,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 ""
@@ -1051,6 +1111,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV 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."
@@ -1168,6 +1246,12 @@ msgstr "Esta libreta no pudo ser guardada: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1225,8 +1309,62 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid "Note title:"
#~ msgstr "Título de nota:"
#~ msgid "Give focus to next pane"
#~ msgstr "Dar enfoque al siguiente panel"
#~ msgid "Give focus to previous pane"
#~ msgstr "Dar enfoque al panel anterior"
#~ msgid "Enter command line mode"
#~ msgstr "Entrar modo linea de comandos"
#~ msgid "Exit command line mode"
#~ msgstr "Salir modo linea de comandos"
#~ msgid "Edit the selected note"
#~ msgstr "Editar la nota seleccionada"
#~ msgid "Cancel the current command."
#~ msgstr "Cancelar el comando actual."
#~ msgid "Exit the application."
#~ msgstr "Salir de la aplicación."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Eliminar la nota o libreta seleccionada."
#, fuzzy
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Marca una tarea como completado / no completado"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "[c]ambia la [c]onsola entre maximizado/minimizado/oculto/visible."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "[c]ambia los [m]etadatos de una nota."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "[H]acer una [n]ota nueva"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "[H]acer una nueva [t]area"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "[H]acer una nueva [l]ibreta"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copiar ([Y]ank) la [n]ota a una libreta."
#~ msgid "Move the note to a notebook."
#~ msgstr "Mover la nota a una libreta."
#, 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 "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Lucas Vieites\n"
"Last-Translator: Fernando Martín <f@mrtn.es>\n"
"Language-Team: Spanish <lucas.vieites@gmail.com>\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
@@ -17,63 +17,12 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
msgid "Give focus to next pane"
msgstr "Enfocar el siguiente panel"
msgid "Give focus to previous pane"
msgstr "Enfocar el panel anterior"
msgid "Enter command line mode"
msgstr "Entrar en modo línea de comandos"
msgid "Exit command line mode"
msgstr "Salir del modo línea de comandos"
msgid "Edit the selected note"
msgstr "Editar la nota seleccionada"
msgid "Cancel the current command."
msgstr "Cancelar el comando actual."
msgid "Exit the application."
msgstr "Salir de la aplicación."
msgid "Delete the currently selected note or notebook."
msgstr "Eliminar la nota o libreta seleccionada."
msgid "To delete a tag, untag the associated notes."
msgstr "Desmarque las notas asociadas para eliminar una etiqueta."
msgid "Please select the note or notebook to be deleted first."
msgstr "Seleccione primero la nota o libreta que desea eliminar."
msgid "Set a to-do as completed / not completed"
msgstr "Marca una tarea como completada/no completada"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "in[t]ercambia la [c]onsola entre maximizada/minimizada/oculta/visible."
msgid "Search"
msgstr "Buscar"
msgid "[t]oggle note [m]etadata."
msgstr "in[t]ercambia los [m]etadatos de una nota."
msgid "[M]ake a new [n]ote"
msgstr "[C]rear una [n]ota nueva"
msgid "[M]ake a new [t]odo"
msgstr "[C]rear una [t]area nueva"
msgid "[M]ake a new note[b]ook"
msgstr "[C]rear una li[b]reta nueva"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copiar ([Y]ank) la [n]ota a una libreta."
msgid "Move the note to a notebook."
msgstr "Mover la nota a una libreta."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Pulse Ctrl+D o escriba «salir» para salir de la aplicación"
@@ -112,7 +61,7 @@ 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 ""
msgstr "No se puede cambiar el elemento cifrado"
#, javascript-format
msgid "Missing required argument: %s"
@@ -174,37 +123,40 @@ msgstr "Marca una tarea como hecha."
#, javascript-format
msgid "Note is not a to-do: \"%s\""
msgstr "Una nota no es una tarea: \"%s\""
msgstr "La nota no es una tarea: \"%s\""
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Manejar la configuración E2EE. Comandos disponibles `enable`, `disable`, "
"`decrypt`, `status` y `target-status`."
msgid "Enter master password:"
msgstr ""
msgstr "Introduce la contraseña maestra:"
msgid "Operation cancelled"
msgstr ""
msgstr "Operación cancelada"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Iniciando descifrado... Por favor espere, puede tardar varios minutos "
"dependiendo de cuanto haya que descifrar."
msgid "Completed decryption."
msgstr ""
msgstr "Descifrado completado."
#, fuzzy
msgid "Enabled"
msgstr "Deshabilitado"
msgstr "Habilitado"
msgid "Disabled"
msgstr "Deshabilitado"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgstr "El cifrado es: %s"
msgid "Edit note."
msgstr "Editar una nota."
@@ -225,8 +177,12 @@ 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 "Error abriendo la nota en el editor: %s"
msgid "Note has been saved."
msgstr "La nota a sido guardada."
msgstr "La nota ha sido guardada."
msgid "Exits the application."
msgstr "Sale de la aplicación."
@@ -250,6 +206,10 @@ msgstr "Mostrar geolocalización de la URL para la nota."
msgid "Displays usage information."
msgstr "Muestra información de uso."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Atajos no disponibles en modo CLI."
@@ -293,8 +253,9 @@ msgstr "Para entrar a modo linea de comando, presiona \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Para salir de modo linea de comando, presiona ESCAPE"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Para una lista completa de los atajos de teclado disponibles, escribe `help "
"shortcuts`"
@@ -430,7 +391,7 @@ msgid "%d notes match this pattern. Delete them?"
msgstr "%d notas coinciden con el patron. Eliminarlas?"
msgid "Delete note?"
msgstr "Eliminar nota?"
msgstr "¿Eliminar nota?"
msgid "Searches for the given <pattern> in all the notes."
msgstr "Buscar el patron <pattern> en todas las notas."
@@ -458,6 +419,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 "No autentificado con %s. Por favor provea las credenciales."
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -471,10 +440,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)"
@@ -597,6 +562,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Uno o más elementos están cifrados y debes proporcionar la contraseña "
"maestra. Para hacerlo por favor escribe `e2ee decrypt`. Si ya has "
"proporcionado la contraseña, los elementos están siendo descifrados en "
"segundo plano y estarán disponibles en breve."
msgid "File"
msgstr "Archivo"
@@ -616,6 +585,10 @@ msgstr "Importar notas de Evernote"
msgid "Evernote Export Files"
msgstr "Archivos exportados de Evernote"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Salir"
@@ -634,6 +607,12 @@ msgstr "Pegar"
msgid "Search in all the notes"
msgstr "Buscar en todas las notas"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Herramientas"
@@ -641,11 +620,10 @@ msgid "Synchronisation status"
msgstr "Estado de la sincronización"
msgid "Encryption options"
msgstr ""
msgstr "Opciones de cifrado"
#, fuzzy
msgid "General Options"
msgstr "Opciones"
msgstr "Opciones generales"
msgid "Help"
msgstr "Ayuda"
@@ -653,6 +631,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentación"
msgid "Check for updates..."
msgstr "Comprobar actualizaciones..."
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -660,12 +641,44 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, javascript-format
msgid "Open %s"
msgstr "Abrir %s"
msgid "Exit"
msgstr "Salir"
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
"Notas de la versión:\n"
"\n"
"%s"
msgid "An update is available, do you want to download it now?"
msgstr "Hay disponible una actualización. ¿Quiere descargarla ahora?"
msgid "Yes"
msgstr "Sí"
msgid "No"
msgstr "No"
msgid "Current version is up-to-date."
msgstr "La versión actual está actualizada."
msgid "Check synchronisation configuration"
msgstr "Comprobar sincronización"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Las notas y los ajustes se guardan en: %s"
@@ -678,6 +691,8 @@ msgid ""
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Deshabilitar el cifrado significa que *todas* tus notas y adjuntos van a ser "
"re-sincronizados y se enviarán descifrados al destino. ¿Deseas continuar?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@@ -685,18 +700,22 @@ 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 ""
"Habilitar el cifrado significa que *todas* tus notas y adjuntos van a ser re-"
"sincronizados y se enviarán cifrados al destino. No pierdas la contraseña, "
"por cuestiones de seguridad, ¡es la *única* forma de descifrar los datos! "
"Para habilitar el cifrado, por favor introduce tu contraseña más abajo."
msgid "Disable encryption"
msgstr ""
msgstr "Deshabilitar cifrado"
msgid "Enable encryption"
msgstr ""
msgstr "Habilitar cifrado"
msgid "Master Keys"
msgstr ""
msgstr "Clave maestra"
msgid "Active"
msgstr ""
msgstr "Activo"
msgid "ID"
msgstr "ID"
@@ -711,22 +730,38 @@ msgid "Updated"
msgstr "Actualizado"
msgid "Password"
msgstr ""
msgstr "Contraseña"
msgid "Password OK"
msgstr ""
msgstr "Contraseña 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 ""
"Nota: Solo una clave maestra va a ser utilizar para el cifrado (la marcada "
"como \"activa\"). Cualquiera de las claves puede ser utilizada para "
"descifrar, dependiendo de como fueron cifradas originalmente las notas o las "
"libretas."
msgid "Missing Master Keys"
msgstr "No se encuentra la clave maestra"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
"La clave maestra con estos ID son utilizadas para descifrar algunos de tus "
"elementos, pero la apliación no tiene acceso a ellas. Serán descargadas a "
"través de la sincronización."
msgid "Status"
msgstr "Estado"
msgid "Encryption is:"
msgstr ""
msgstr "El cifrado está:"
msgid "Back"
msgstr "Atrás"
@@ -757,6 +792,9 @@ msgstr "Renombrar libreta:"
msgid "Set alarm:"
msgstr "Ajustar alarma:"
msgid "Search"
msgstr "Buscar"
msgid "Layout"
msgstr "Diseño"
@@ -766,12 +804,11 @@ msgstr "No se han podido sincronizar algunos de los elementos."
msgid "View them now"
msgstr "Verlos ahora"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "No se han podido sincronizar algunos de los elementos."
msgstr "No se han podido descifrar algunos elementos."
msgid "Set the password"
msgstr ""
msgstr "Establecer la contraseña"
msgid "Add or remove tags"
msgstr "Añadir o borrar etiquetas"
@@ -792,6 +829,12 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "No hay ninguna libreta. Cree una pulsando en «Libreta nueva»."
msgid "Open..."
msgstr "Abrir..."
msgid "Save as..."
msgstr "Guardar como..."
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Enlace o mensaje no soportado: %s"
@@ -799,8 +842,21 @@ msgstr "Enlace o mensaje no soportado: %s"
msgid "Attach file"
msgstr "Adjuntar archivo"
msgid "Tags"
msgstr "Etiquetas"
msgid "Set alarm"
msgstr "Fijar alarma"
msgstr "Establecer alarma"
msgid "to-do"
msgstr "lista de tareas"
msgid "note"
msgstr "nota"
#, javascript-format
msgid "Creating new %s..."
msgstr "Creando nuevo %s..."
msgid "Refresh"
msgstr "Refrescar"
@@ -821,7 +877,7 @@ msgid "Synchronisation Status"
msgstr "Estado de la sincronización"
msgid "Encryption Options"
msgstr ""
msgstr "Opciones de cifrado"
msgid "Remove this tag from all the notes?"
msgstr "¿Desea eliminar esta etiqueta de todas las notas?"
@@ -838,9 +894,6 @@ msgstr "Sincronizar"
msgid "Notebooks"
msgstr "Libretas"
msgid "Tags"
msgstr "Etiquetas"
msgid "Searches"
msgstr "Búsquedas"
@@ -858,12 +911,20 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
#, fuzzy
msgid "Nextcloud"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Solo para pruebas)"
#, fuzzy
msgid "WebDAV"
msgstr "Servidor WebDAV"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -922,9 +983,9 @@ msgstr "Elementos locales borrados: %d."
msgid "Deleted remote items: %d."
msgstr "Elementos remotos borrados: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Elementos locales creados: %d."
msgstr "Elementos obtenidos: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
@@ -942,11 +1003,10 @@ msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronización ya está en progreso. Estado: %s"
msgid "Encrypted"
msgstr ""
msgstr "Cifrado"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "No se han podido sincronizar algunos de los elementos."
msgstr "Los elementos cifrados no puedes ser modificados"
msgid "Conflicts"
msgstr "Conflictos"
@@ -1002,12 +1062,34 @@ msgstr "Claro"
msgid "Dark"
msgstr "Oscuro"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
msgid "When creating a new to-do:"
msgstr "Al crear una nueva lista de tareas:"
msgid "Focus title"
msgstr "Foco en el título:"
msgid "Focus body"
msgstr "Foco en el cuerpo"
msgid "When creating a new note:"
msgstr "Cuando se crear una nota nueva:"
msgid "Show tray icon"
msgstr "Mostrar icono en la bandeja"
msgid "Set application zoom percentage"
msgstr "Establecer el porcentaje de aumento de la aplicación"
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -1023,9 +1105,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"
@@ -1033,11 +1112,12 @@ 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»."
"El destino de la sincronización. Cada destino de la sincronización puede "
"tener parámetros adicionales los cuales son llamados como `sync.NUM.NAME` "
"(todos abajo documentados)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Directorio con el que sincronizarse (ruta completa)"
@@ -1049,6 +1129,24 @@ 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 "Servidor Nexcloud WebDAV"
msgid "Nexcloud username"
msgstr "Usuario de Nexcloud"
msgid "Nexcloud password"
msgstr "Contraseña de Nexcloud"
msgid "WebDAV URL"
msgstr "Servidor WebDAV"
msgid "WebDAV username"
msgstr "Usuario de WebDAV"
msgid "WebDAV password"
msgstr "Contraseña de WebDAV"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1056,15 +1154,18 @@ msgstr "Opción inválida: «%s». Los valores posibles son: %s."
msgid "Items that cannot be synchronised"
msgstr "Elementos que no se pueden sincronizar"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%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 ""
"Estos elementos se mantendrán en el dispositivo pero no serán enviados al "
"destino de sincronización. Para encontrar dichos elementos busca en el "
"título o en el ID (el cual se muestra arriba entre corchetes)."
msgid "Sync status (synced items / total items)"
msgstr "Estado de sincronización (elementos sincronizados/elementos totales)"
@@ -1112,7 +1213,7 @@ msgid "Export Debug Report"
msgstr "Exportar informe de depuración"
msgid "Encryption Config"
msgstr ""
msgstr "Configuración de cifrado"
msgid "Configuration"
msgstr "Configuración"
@@ -1125,7 +1226,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "¿Desea mover %d notas a libreta «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Presiona para establecer la contraseña de descifrado."
msgid "Select date"
msgstr "Seleccione fecha"
@@ -1138,21 +1239,20 @@ msgstr "Cancelar sincronización"
#, javascript-format
msgid "Master Key %s"
msgstr ""
msgstr "Clave maestra %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Creado: %d."
msgstr "Creado: %s"
msgid "Password:"
msgstr ""
msgstr "Contraseña:"
msgid "Password cannot be empty"
msgstr ""
msgstr "La contraseña no puede estar vacía"
#, fuzzy
msgid "Enable"
msgstr "Deshabilitado"
msgstr "Habilitado"
#, javascript-format
msgid "The notebook could not be saved: %s"
@@ -1161,6 +1261,12 @@ msgstr "No se ha podido guardar esta libreta: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr "Mostrar todo"
msgid "Errors only"
msgstr "Solo errores"
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1215,8 +1321,67 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid "Note title:"
#~ msgstr "Título de la nota:"
#~ msgid "Give focus to next pane"
#~ msgstr "Enfocar el siguiente panel"
#~ msgid "Give focus to previous pane"
#~ msgstr "Enfocar el panel anterior"
#~ msgid "Enter command line mode"
#~ msgstr "Entrar en modo línea de comandos"
#~ msgid "Exit command line mode"
#~ msgstr "Salir del modo línea de comandos"
#~ msgid "Edit the selected note"
#~ msgstr "Editar la nota seleccionada"
#~ msgid "Cancel the current command."
#~ msgstr "Cancelar el comando actual."
#~ msgid "Exit the application."
#~ msgstr "Salir de la aplicación."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Eliminar la nota o libreta seleccionada."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Marca una tarea como completada/no completada"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "in[t]ercambia la [c]onsola entre maximizada/minimizada/oculta/visible."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "in[t]ercambia los [m]etadatos de una nota."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "[C]rear una [n]ota nueva"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "[C]rear una [t]area nueva"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "[C]rear una li[b]reta nueva"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copiar ([Y]ank) la [n]ota a una libreta."
#~ msgid "Move the note to a notebook."
#~ msgstr "Mover la nota a una libreta."
#~ msgid "Error"
#~ msgstr "Error"
#~ msgid "WebDAV (Beta)"
#~ msgstr "WebDAV (Beta)"
#~ 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 "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

1372
CliClient/locales/eu.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Last-Translator: Laurent Cozic\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
@@ -15,63 +15,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.3\n"
msgid "Give focus to next pane"
msgstr "Activer le volet suivant"
msgid "Give focus to previous pane"
msgstr "Activer le volet précédent"
msgid "Enter command line mode"
msgstr "Démarrer le mode de ligne de commande"
msgid "Exit command line mode"
msgstr "Sortir du mode de ligne de commande"
msgid "Edit the selected note"
msgstr "Éditer la note sélectionnée"
msgid "Cancel the current command."
msgstr "Annuler la commande en cours."
msgid "Exit the application."
msgstr "Quitter le logiciel."
msgid "Delete the currently selected note or notebook."
msgstr "Supprimer la note ou carnet sélectionné."
msgid "To delete a tag, untag the associated notes."
msgstr "Pour supprimer une vignette, enlever là des notes associées."
msgid "Please select the note or notebook to be deleted first."
msgstr "Veuillez d'abord sélectionner un carnet."
msgid "Set a to-do as completed / not completed"
msgstr "Marquer une tâches comme complétée / non-complétée"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
msgid "Search"
msgstr "Chercher"
msgid "[t]oggle note [m]etadata."
msgstr "Afficher/Cacher les métadonnées des notes."
msgid "[M]ake a new [n]ote"
msgstr "Créer une nouvelle note"
msgid "[M]ake a new [t]odo"
msgstr "Créer une nouvelle tâche"
msgid "[M]ake a new note[b]ook"
msgstr "Créer un nouveau carnet"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copier la note dans un autre carnet."
msgid "Move the note to a notebook."
msgstr "Déplacer la note vers un carnet."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Appuyez sur Ctrl+D ou tapez \"exit\" pour sortir du logiciel"
@@ -228,6 +177,10 @@ msgstr ""
"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à "
"l'invite de commande."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Erreur lors de l'ouverture de la note dans l'éditeur de texte : %s"
msgid "Note has been saved."
msgstr "La note a été enregistrée."
@@ -254,6 +207,10 @@ msgstr "Afficher l'URL de l'emplacement de la note."
msgid "Displays usage information."
msgstr "Affiche les informations d'utilisation."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Les raccourcis ne sont pas disponible en mode de ligne de commande."
@@ -296,8 +253,9 @@ msgstr "Pour démarrer le mode ligne de commande, pressez \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Pour sortir du mode ligne de commande, pressez ECHAP"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Pour la liste complète des raccourcis disponibles, tapez `help shortcuts`"
@@ -458,6 +416,16 @@ msgstr ""
"Synchroniser avec la cible donnée (par défaut, la valeur de configuration "
"`sync.target`)."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Non-connecté à %s. Veuillez fournir les identifiants et mots de passe "
"manquants."
msgid "Synchronisation is already in progress."
msgstr "La synchronisation est déjà en cours."
@@ -471,10 +439,6 @@ msgstr ""
"correctement. Si vous savez qu'aucune autre synchronisation est en cours, "
"vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Cible de la synchronisation : %s (%s)"
@@ -618,6 +582,10 @@ msgstr "Importer notes d'Evernote"
msgid "Evernote Export Files"
msgstr "Fichiers d'export Evernote"
#, javascript-format
msgid "Hide %s"
msgstr "Cacher %s"
msgid "Quit"
msgstr "Quitter"
@@ -636,6 +604,12 @@ msgstr "Coller"
msgid "Search in all the notes"
msgstr "Chercher dans toutes les notes"
msgid "View"
msgstr "Affichage"
msgid "Toggle editor layout"
msgstr "Basculer l'agencement de l'éditeur"
msgid "Tools"
msgstr "Outils"
@@ -654,6 +628,9 @@ msgstr "Aide"
msgid "Website and documentation"
msgstr "Documentation en ligne"
msgid "Check for updates..."
msgstr "Vérifier les mises à jour..."
msgid "About Joplin"
msgstr "A propos de Joplin"
@@ -661,11 +638,44 @@ msgstr "A propos de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, javascript-format
msgid "Open %s"
msgstr "Ouvrir %s"
msgid "Exit"
msgstr "Quitter"
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annulation"
msgstr "Annuler"
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
"Notes de version :\n"
"\n"
"%s"
msgid "An update is available, do you want to download it now?"
msgstr ""
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
msgid "Check synchronisation configuration"
msgstr "Vérifier config synchronisation"
#, javascript-format
msgid "Notes and settings are stored in: %s"
@@ -735,6 +745,18 @@ msgstr ""
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
"à l'origine."
msgid "Missing Master Keys"
msgstr "Clefs maître manquantes"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
"probable qu'elle vont être prochainement disponible via la synchronisation."
msgid "Status"
msgstr "État"
@@ -772,6 +794,9 @@ msgstr "Renommer le carnet :"
msgid "Set alarm:"
msgstr "Régler alarme :"
msgid "Search"
msgstr "Chercher"
msgid "Layout"
msgstr "Disposition"
@@ -809,6 +834,12 @@ msgstr ""
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
"carnet\"."
msgid "Open..."
msgstr "Ouvrir..."
msgid "Save as..."
msgstr "Enregistrer sous..."
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Lien ou message non géré : %s"
@@ -816,9 +847,22 @@ msgstr "Lien ou message non géré : %s"
msgid "Attach file"
msgstr "Attacher un fichier"
msgid "Tags"
msgstr "Étiquettes"
msgid "Set alarm"
msgstr "Régler alarme"
msgid "to-do"
msgstr "tâche"
msgid "note"
msgstr "note"
#, javascript-format
msgid "Creating new %s..."
msgstr "Création de %s..."
msgid "Refresh"
msgstr "Rafraîchir"
@@ -855,9 +899,6 @@ msgstr "Synchroniser"
msgid "Notebooks"
msgstr "Carnets"
msgid "Tags"
msgstr "Étiquettes"
msgid "Searches"
msgstr "Recherches"
@@ -876,12 +917,18 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud"
msgstr "Nextcloud"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
msgid "WebDAV"
msgstr "WebDAV"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@@ -1018,12 +1065,33 @@ msgstr "Clair"
msgid "Dark"
msgstr "Sombre"
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
msgid "When creating a new to-do:"
msgstr "Lors de la création d'une tâche :"
msgid "Focus title"
msgstr "Curseur sur le titre"
msgid "Focus body"
msgstr "Curseur sur corps du message"
msgid "When creating a new note:"
msgstr "Lors de la création d'une note :"
msgid "Show tray icon"
msgstr "Afficher icône dans la zone de notifications"
msgid "Set application zoom percentage"
msgstr "Niveau de zoom"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Synchronisation interval"
msgstr "Intervalle de synchronisation"
@@ -1039,9 +1107,6 @@ msgstr "%d heure"
msgid "%d hours"
msgstr "%d heures"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Show advanced options"
msgstr "Montrer les options avancées"
@@ -1049,11 +1114,12 @@ msgid "Synchronisation target"
msgstr "Cible de la synchronisation"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
"La cible avec laquelle synchroniser. Chaque cible de synchronisation peut "
"avoir des paramètres supplémentaires sous le nom `sync.NUM.NOM` (documentés "
"ci-dessous)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
@@ -1065,6 +1131,24 @@ 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"
msgid "WebDAV URL"
msgstr "WebDAV : URL"
msgid "WebDAV username"
msgstr "WebDAV : Nom utilisateur"
msgid "WebDAV password"
msgstr "WebDAV : Mot de passe"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
@@ -1139,7 +1223,7 @@ msgid "Configuration"
msgstr "Configuration"
msgid "Move to notebook..."
msgstr "Déplacer la note vers carnet..."
msgstr "Déplacer vers..."
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
@@ -1181,6 +1265,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
msgid "Edit notebook"
msgstr "Éditer le carnet"
msgid "Show all"
msgstr "Afficher tous"
msgid "Errors only"
msgstr "Erreurs seulement"
msgid "This note has been modified:"
msgstr "Cette note a été modifiée :"
@@ -1236,8 +1326,77 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ msgid "Note title:"
#~ msgstr "Titre de la note :"
#~ msgid "Give focus to next pane"
#~ msgstr "Activer le volet suivant"
#~ msgid "Give focus to previous pane"
#~ msgstr "Activer le volet précédent"
#~ msgid "Enter command line mode"
#~ msgstr "Démarrer le mode de ligne de commande"
#~ msgid "Exit command line mode"
#~ msgstr "Sortir du mode de ligne de commande"
#~ msgid "Edit the selected note"
#~ msgstr "Éditer la note sélectionnée"
#~ msgid "Cancel the current command."
#~ msgstr "Annuler la commande en cours."
#~ msgid "Exit the application."
#~ msgstr "Quitter le logiciel."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Supprimer la note ou carnet sélectionné."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Marquer une tâches comme complétée / non-complétée"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "Afficher/Cacher les métadonnées des notes."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "Créer une nouvelle note"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "Créer une nouvelle tâche"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "Créer un nouveau carnet"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copier la note dans un autre carnet."
#~ msgid "Move the note to a notebook."
#~ msgstr "Déplacer la note vers un carnet."
#~ msgid "Error"
#~ msgstr "Erreur"
#~ msgid "WebDAV (Beta)"
#~ msgstr "WebDAV (Bêta)"
#~ msgid "Could not download the update: %s"
#~ msgstr "Impossible de télécharger la mise à jour : %s"
#~ msgid "New version downloaded - application will quit now and update..."
#~ msgstr ""
#~ "La nouvelle version a été téléchargée - le programme va se fermer et se "
#~ "mettre à jour..."
#~ msgid "Could not install the update: %s"
#~ msgstr "Impossible d'installer la mise à jour : %s"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
#~ "fichier, veuillez spécifier le répertoire avec `sync.2.path`."
#~ msgid "To-do title:"
#~ msgstr "Titre de la tâche :"
@@ -1303,9 +1462,6 @@ msgstr "Bienvenue"
#~ msgid "Todo filter"
#~ msgstr "Filtre des tâches"
#~ msgid "Show all"
#~ msgstr "Afficher tous"
#~ msgid "Non-completed and recently completed ones"
#~ msgstr "Tâches non-complétées et récentes"

View File

@@ -17,69 +17,12 @@ msgstr ""
"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"
msgid "Give focus to next pane"
msgstr "Fokusiraj sljedeće okno"
msgid "Give focus to previous pane"
msgstr "Fokusiraj prethodno okno"
msgid "Enter command line mode"
msgstr "Otvori naredbeni redak"
msgid "Exit command line mode"
msgstr "Napusti naredbeni redak"
msgid "Edit the selected note"
msgstr "Uredi odabranu bilješku"
msgid "Cancel the current command."
msgstr "Prekini trenutnu naredbu."
msgid "Exit the application."
msgstr "Izađi iz aplikacije."
msgid "Delete the currently selected note or notebook."
msgstr "Obriši odabranu bilješku ili bilježnicu."
msgid "To delete a tag, untag the associated notes."
msgstr "Da bi mogao obrisati oznaku, skini oznaku s povezanih bilješki."
msgid "Please select the note or notebook to be deleted first."
msgstr "Odaberi bilješku ili bilježnicu za brisanje."
msgid "Set a to-do as completed / not completed"
msgstr "Postavi zadatak kao završen/nezavršen"
#, fuzzy
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgid "Search"
msgstr "Traži"
#, fuzzy
msgid "[t]oggle note [m]etadata."
msgstr "[t]oggle note [m]etadata."
#, fuzzy
msgid "[M]ake a new [n]ote"
msgstr "[M]ake a new [n]ote"
#, fuzzy
msgid "[M]ake a new [t]odo"
msgstr "[M]ake a new [t]odo"
#, fuzzy
msgid "[M]ake a new note[b]ook"
msgstr "[M]ake a new note[b]ook"
#, fuzzy
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copy ([Y]ank) the [n]ote to a notebook."
msgid "Move the note to a notebook."
msgstr "Premjesti bilješku u bilježnicu."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Pritisni Ctrl+D ili napiši \"exit\" za izlazak iz aplikacije"
@@ -235,6 +178,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."
@@ -260,6 +207,10 @@ msgstr "Prikazuje geolokacijski URL bilješke."
msgid "Displays usage information."
msgstr "Prikazuje informacije o korištenju."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Prečaci nisu podržani u naredbenom retku."
@@ -303,8 +254,9 @@ msgstr "Za ulaz u naredbeni redak, pritisni \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Za izlaz iz naredbenog retka, pritisni Esc"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr "Za potpunu listu mogućih prečaca, upiši `help shortcuts`"
msgid "Imports an Evernote notebook file (.enex file)."
@@ -466,6 +418,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."
@@ -477,12 +439,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)"
@@ -624,6 +580,10 @@ msgstr "Uvezi Evernote bilješke"
msgid "Evernote Export Files"
msgstr "Evernote izvozne datoteke"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Izađi"
@@ -642,6 +602,12 @@ msgstr "Zalijepi"
msgid "Search in all the notes"
msgstr "Pretraži u svim bilješkama"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Alati"
@@ -661,6 +627,9 @@ msgstr "Pomoć"
msgid "Website and documentation"
msgstr "Website i dokumentacija"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "O Joplinu"
@@ -668,12 +637,43 @@ msgstr "O Joplinu"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "On %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "U redu"
msgid "Cancel"
msgstr "Odustani"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Obriši bilješke?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Prekini sinkronizaciju"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Bilješke i postavke su pohranjene u: %s"
@@ -730,6 +730,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -767,6 +776,9 @@ msgstr "Preimenuj bilježnicu:"
msgid "Set alarm:"
msgstr "Postavi upozorenje:"
msgid "Search"
msgstr "Traži"
msgid "Layout"
msgstr "Izgled"
@@ -802,6 +814,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "Ovdje nema bilježnica. Stvori novu pritiskom na \"Nova bilježnica\"."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Spremi promjene"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Nepodržana poveznica ili poruka: %s"
@@ -809,9 +828,24 @@ msgstr "Nepodržana poveznica ili poruka: %s"
msgid "Attach file"
msgstr "Priloži datoteku"
msgid "Tags"
msgstr "Oznake"
msgid "Set alarm"
msgstr "Postavi upozorenje"
#, fuzzy
msgid "to-do"
msgstr "Novi zadatak"
#, fuzzy
msgid "note"
msgstr "Nova bilješka"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Uvozim bilješke..."
msgid "Refresh"
msgstr "Osvježi"
@@ -848,9 +882,6 @@ msgstr "Sinkroniziraj"
msgid "Notebooks"
msgstr "Bilježnice"
msgid "Tags"
msgstr "Oznake"
msgid "Searches"
msgstr "Pretraživanja"
@@ -868,12 +899,18 @@ msgstr "Nepoznata zastavica: %s"
msgid "File system"
msgstr "Datotečni sustav"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Samo za testiranje)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nepoznata razina logiranja: %s"
@@ -1009,12 +1046,37 @@ msgstr "Svijetla"
msgid "Dark"
msgstr "Tamna"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Save geo-location with notes"
msgstr "Spremi geolokacijske podatke sa bilješkama"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Stvara novi zadatak."
#, fuzzy
msgid "Focus title"
msgstr "Naslov bilješke:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Stvara novu bilješku."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Synchronisation interval"
msgstr "Interval sinkronizacije"
@@ -1030,9 +1092,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"
@@ -1040,11 +1099,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)"
@@ -1056,6 +1113,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@@ -1168,6 +1243,12 @@ msgstr "Bilježnicu nije moguće snimiti: %s"
msgid "Edit notebook"
msgstr "Uredi bilježnicu"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Bilješka je promijenjena:"
@@ -1221,8 +1302,66 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
msgid "Welcome"
msgstr "Dobro došli"
#~ msgid "Note title:"
#~ msgstr "Naslov bilješke:"
#~ msgid "Give focus to next pane"
#~ msgstr "Fokusiraj sljedeće okno"
#~ msgid "Give focus to previous pane"
#~ msgstr "Fokusiraj prethodno okno"
#~ msgid "Enter command line mode"
#~ msgstr "Otvori naredbeni redak"
#~ msgid "Exit command line mode"
#~ msgstr "Napusti naredbeni redak"
#~ msgid "Edit the selected note"
#~ msgstr "Uredi odabranu bilješku"
#~ msgid "Cancel the current command."
#~ msgstr "Prekini trenutnu naredbu."
#~ msgid "Exit the application."
#~ msgstr "Izađi iz aplikacije."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Obriši odabranu bilješku ili bilježnicu."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Postavi zadatak kao završen/nezavršen"
#, fuzzy
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#, fuzzy
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "[t]oggle note [m]etadata."
#, fuzzy
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "[M]ake a new [n]ote"
#, fuzzy
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "[M]ake a new [t]odo"
#, fuzzy
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "[M]ake a new note[b]ook"
#, fuzzy
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgid "Move the note to a notebook."
#~ msgstr "Premjesti bilješku u bilježnicu."
#~ 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 "To-do title:"
#~ msgstr "Naslov zadatka:"

View File

@@ -16,65 +16,12 @@ msgstr ""
"X-Generator: Poedit 2.0.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
msgstr "Pannello successivo"
msgid "Give focus to previous pane"
msgstr "Pannello precedente"
msgid "Enter command line mode"
msgstr "Accedi alla modalità linea di comando"
msgid "Exit command line mode"
msgstr "Esci dalla modalità linea di comando"
msgid "Edit the selected note"
msgstr "Modifica la nota selezionata"
msgid "Cancel the current command."
msgstr "Cancella il comando corrente."
msgid "Exit the application."
msgstr "Esci dall'applicazione."
msgid "Delete the currently selected note or notebook."
msgstr "Elimina la nota o il blocco note selezionato."
msgid "To delete a tag, untag the associated notes."
msgstr "Elimina un'etichetta, togli l'etichetta associata alle note."
msgid "Please select the note or notebook to be deleted first."
msgstr "Per favore seleziona la nota o il blocco note da eliminare."
msgid "Set a to-do as completed / not completed"
msgstr "Imposta un'attività come completata / non completata"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
"Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/"
"visibile."
msgid "Search"
msgstr "Cerca"
msgid "[t]oggle note [m]etadata."
msgstr "mos[t]ra/nascondi i [m]etadata nelle note."
msgid "[M]ake a new [n]ote"
msgstr "Crea ([M]ake) una nuova [n]ota"
msgid "[M]ake a new [t]odo"
msgstr "Crea ([M]ake) una nuova at[t]ività"
msgid "[M]ake a new note[b]ook"
msgstr "Crea ([M]ake) un nuovo [b]locco note"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copia ([Y]) la [n]ota in un blocco note."
msgid "Move the note to a notebook."
msgstr "Sposta la nota in un blocco note."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Premi Ctrl+D o digita \"exit\" per uscire dall'applicazione"
@@ -226,6 +173,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."
@@ -252,6 +203,10 @@ msgstr "Mostra l'URL di geolocalizzazione per la nota."
msgid "Displays usage information."
msgstr "Mostra le informazioni di utilizzo."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Le scorciatoie non sono disponibili nella modalità CLI."
@@ -294,8 +249,9 @@ msgstr "Per entrare nella modalità command line, premi \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Per uscire dalla modalità command line, premi ESC"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Per la lista completa delle scorciatoie disponibili, digita `help shortcuts`"
@@ -449,6 +405,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."
@@ -462,12 +428,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)"
@@ -602,6 +562,10 @@ msgstr "Importa le note da Evernote"
msgid "Evernote Export Files"
msgstr "Esposta i files di Evernote"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Esci"
@@ -620,6 +584,12 @@ msgstr "Incolla"
msgid "Search in all the notes"
msgstr "Cerca in tutte le note"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Strumenti"
@@ -639,6 +609,9 @@ msgstr "Aiuto"
msgid "Website and documentation"
msgstr "Sito web e documentazione"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Informazione si Joplin"
@@ -646,12 +619,43 @@ msgstr "Informazione si Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Su %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancella"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Eliminare le note?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Cancella la sincronizzazione"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -710,6 +714,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Stato"
@@ -745,6 +758,9 @@ msgstr "Rinomina il blocco note:"
msgid "Set alarm:"
msgstr "Imposta allarme:"
msgid "Search"
msgstr "Cerca"
msgid "Layout"
msgstr "Disposizione"
@@ -781,6 +797,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "Al momento non ci sono note. Creane una cliccando sul bottone (+)."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Salva i cambiamenti"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Collegamento o messaggio non supportato: %s"
@@ -788,9 +811,24 @@ msgstr "Collegamento o messaggio non supportato: %s"
msgid "Attach file"
msgstr "Allega file"
msgid "Tags"
msgstr "Etichette"
msgid "Set alarm"
msgstr "Imposta allarme"
#, fuzzy
msgid "to-do"
msgstr "Nuova attività"
#, fuzzy
msgid "note"
msgstr "Nuova nota"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Importazione delle note..."
msgid "Refresh"
msgstr "Aggiorna"
@@ -827,9 +865,6 @@ msgstr "Sincronizza"
msgid "Notebooks"
msgstr "Blocchi note"
msgid "Tags"
msgstr "Etichette"
msgid "Searches"
msgstr "Ricerche"
@@ -848,12 +883,18 @@ msgstr "Etichetta sconosciuta: %s"
msgid "File system"
msgstr "File system"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (solo per test)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Livello di log sconosciuto: %s"
@@ -991,12 +1032,37 @@ msgstr "Chiaro"
msgid "Dark"
msgstr "Scuro"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Save geo-location with notes"
msgstr "Salva geo-localizzazione con le note"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nuova attività."
#, fuzzy
msgid "Focus title"
msgstr "Titolo della Nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nuova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Synchronisation interval"
msgstr "Intervallo di sincronizzazione"
@@ -1012,9 +1078,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"
@@ -1022,12 +1085,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 ""
@@ -1039,6 +1099,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
@@ -1151,6 +1229,12 @@ msgstr "Il blocco note non può essere salvato: %s"
msgid "Edit notebook"
msgstr "Modifica blocco note"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Questa note è stata modificata:"
@@ -1206,8 +1290,63 @@ msgstr ""
msgid "Welcome"
msgstr "Benvenuto"
#~ msgid "Note title:"
#~ msgstr "Titolo della Nota:"
#~ msgid "Give focus to next pane"
#~ msgstr "Pannello successivo"
#~ msgid "Give focus to previous pane"
#~ msgstr "Pannello precedente"
#~ msgid "Enter command line mode"
#~ msgstr "Accedi alla modalità linea di comando"
#~ msgid "Exit command line mode"
#~ msgstr "Esci dalla modalità linea di comando"
#~ msgid "Edit the selected note"
#~ msgstr "Modifica la nota selezionata"
#~ msgid "Cancel the current command."
#~ msgstr "Cancella il comando corrente."
#~ msgid "Exit the application."
#~ msgstr "Esci dall'applicazione."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Elimina la nota o il blocco note selezionato."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Imposta un'attività come completata / non completata"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/"
#~ "visibile."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "mos[t]ra/nascondi i [m]etadata nelle note."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "Crea ([M]ake) una nuova [n]ota"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "Crea ([M]ake) una nuova at[t]ività"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "Crea ([M]ake) un nuovo [b]locco note"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copia ([Y]) la [n]ota in un blocco note."
#~ msgid "Move the note to a notebook."
#~ msgstr "Sposta la nota in un blocco note."
#~ 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 "To-do title:"
#~ msgstr "Titolo dell'attività:"

View File

@@ -16,63 +16,12 @@ msgstr ""
"X-Generator: Poedit 2.0.5\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Give focus to next pane"
msgstr "次のペインへ"
msgid "Give focus to previous pane"
msgstr "前のペインへ"
msgid "Enter command line mode"
msgstr "コマンドラインモードに入る"
msgid "Exit command line mode"
msgstr "コマンドラインモードの終了"
msgid "Edit the selected note"
msgstr "選択したノートを編集"
msgid "Cancel the current command."
msgstr "現在のコマンドをキャンセル"
msgid "Exit the application."
msgstr "アプリケーションを終了する"
msgid "Delete the currently selected note or notebook."
msgstr "選択中のノートまたはノートブックを削除"
msgid "To delete a tag, untag the associated notes."
msgstr "タグを削除するには、関連するノートからタグを外してください。"
msgid "Please select the note or notebook to be deleted first."
msgstr "ます削除するノートかノートブックを選択してください。"
msgid "Set a to-do as completed / not completed"
msgstr "ToDoを完了/未完に設定"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "コンソールを最大表示/最小表示/非表示/可視で切り替える([t][c])"
msgid "Search"
msgstr "検索"
msgid "[t]oggle note [m]etadata."
msgstr "ノートのメタ情報を切り替える [tm]"
msgid "[M]ake a new [n]ote"
msgstr "新しいノートの作成 [mn]"
msgid "[M]ake a new [t]odo"
msgstr "新しいToDoの作成 [mt]"
msgid "[M]ake a new note[b]ook"
msgstr "新しいノートブックの作成 [mb]"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "ノートをノートブックにコピー [yn]"
msgid "Move the note to a notebook."
msgstr "ノートをノートブックに移動"
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "アプリケーションを終了するには、Ctrl+Dまたは\"exit\"と入力してください"
@@ -223,6 +172,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 "ノートは保存されました。"
@@ -248,6 +201,10 @@ msgstr "ノートの位置情報URLを表示する。"
msgid "Displays usage information."
msgstr "使い方を表示する。"
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "CLIモードではショートカットは使用できません。"
@@ -287,8 +244,9 @@ msgstr "コマンドラインモードに入るには、\":\"を入力してく
msgid "To exit command line mode, press ESCAPE"
msgstr "コマンドラインモードを終了するには、ESCキーを押してください。"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"有効なすべてのキーボードショートカットを表示するには、`help shortcuts`と入力"
"してください。"
@@ -446,6 +404,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 "同期はすでに実行中です。"
@@ -458,10 +424,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)"
@@ -601,6 +563,10 @@ msgstr "Evernoteのインポート"
msgid "Evernote Export Files"
msgstr "Evernote Exportファイル"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "終了"
@@ -619,6 +585,12 @@ msgstr "貼り付け"
msgid "Search in all the notes"
msgstr "すべてのノートを検索"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "ツール"
@@ -638,6 +610,9 @@ msgstr "ヘルプ"
msgid "Website and documentation"
msgstr "Webサイトとドキュメント"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Joplinについて"
@@ -645,12 +620,42 @@ msgstr "Joplinについて"
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr "キャンセル"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "ノートを削除しますか?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "同期の中止"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "ノートと設定は、%sに保存されます。"
@@ -711,6 +716,15 @@ msgstr ""
"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に"
"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。"
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状態"
@@ -748,6 +762,9 @@ msgstr "ノートブックの名前を変更:"
msgid "Set alarm:"
msgstr "アラームをセット:"
msgid "Search"
msgstr "検索"
msgid "Layout"
msgstr "レイアウト"
@@ -783,6 +800,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "ノートブックがありません。新しいノートブックを作成してください。"
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "変更を保存"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr ""
@@ -790,9 +814,24 @@ msgstr ""
msgid "Attach file"
msgstr "ファイルを添付"
msgid "Tags"
msgstr "タグ"
msgid "Set alarm"
msgstr "アラームをセット"
#, fuzzy
msgid "to-do"
msgstr "新しいToDo"
#, fuzzy
msgid "note"
msgstr "新しいノート"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "ノートのインポート…"
msgid "Refresh"
msgstr "更新"
@@ -829,9 +868,6 @@ msgstr "同期"
msgid "Notebooks"
msgstr "ノートブック"
msgid "Tags"
msgstr "タグ"
msgid "Searches"
msgstr "検索"
@@ -849,12 +885,18 @@ msgstr "不明なフラグ: %s"
msgid "File system"
msgstr "ファイルシステム"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -994,12 +1036,37 @@ msgstr "明るい"
msgid "Dark"
msgstr "暗い"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "新しいToDoを作成します。"
#, fuzzy
msgid "Focus title"
msgstr "ノートの題名:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "あたらしいノートを作成します。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Synchronisation interval"
msgstr "同期間隔"
@@ -1015,9 +1082,6 @@ msgstr "%d 時間"
msgid "%d hours"
msgstr "%d 時間"
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Show advanced options"
msgstr "詳細な設定の表示"
@@ -1025,11 +1089,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 "同期先のディレクトリ(絶対パス)"
@@ -1041,6 +1103,24 @@ msgstr ""
"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく"
"ださい。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1153,6 +1233,12 @@ msgstr "ノートブックは保存できませんでした:%s"
msgid "Edit notebook"
msgstr "ノートブックの編集"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "ノートは変更されています:"
@@ -1208,8 +1294,60 @@ msgstr ""
msgid "Welcome"
msgstr "ようこそ"
#~ msgid "Note title:"
#~ msgstr "ノートの題名:"
#~ msgid "Give focus to next pane"
#~ msgstr "次のペインへ"
#~ msgid "Give focus to previous pane"
#~ msgstr "前のペインへ"
#~ msgid "Enter command line mode"
#~ msgstr "コマンドラインモードに入る"
#~ msgid "Exit command line mode"
#~ msgstr "コマンドラインモードの終了"
#~ msgid "Edit the selected note"
#~ msgstr "選択したノートを編集"
#~ msgid "Cancel the current command."
#~ msgstr "現在のコマンドをキャンセル"
#~ msgid "Exit the application."
#~ msgstr "アプリケーションを終了する"
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "選択中のノートまたはノートブックを削除"
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "ToDoを完了/未完に設定"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "コンソールを最大表示/最小表示/非表示/可視で切り替える([t][c])"
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "ノートのメタ情報を切り替える [tm]"
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "新しいノートの作成 [mn]"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "新しいToDoの作成 [mt]"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "新しいノートブックの作成 [mb]"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "ノートをノートブックにコピー [yn]"
#~ msgid "Move the note to a notebook."
#~ 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 "To-do title:"
#~ msgstr "ToDoの題名:"

View File

@@ -15,63 +15,12 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Give focus to next pane"
msgstr ""
msgid "Give focus to previous pane"
msgstr ""
msgid "Enter command line mode"
msgstr ""
msgid "Exit command line mode"
msgstr ""
msgid "Edit the selected note"
msgstr ""
msgid "Cancel the current command."
msgstr ""
msgid "Exit the application."
msgstr ""
msgid "Delete the currently selected note or notebook."
msgstr ""
msgid "To delete a tag, untag the associated notes."
msgstr ""
msgid "Please select the note or notebook to be deleted first."
msgstr ""
msgid "Set a to-do as completed / not completed"
msgstr ""
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
msgid "Search"
msgstr ""
msgid "[t]oggle note [m]etadata."
msgstr ""
msgid "[M]ake a new [n]ote"
msgstr ""
msgid "[M]ake a new [t]odo"
msgstr ""
msgid "[M]ake a new note[b]ook"
msgstr ""
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr ""
msgid "Move the note to a notebook."
msgstr ""
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr ""
@@ -214,6 +163,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -237,6 +190,10 @@ msgstr ""
msgid "Displays usage information."
msgstr ""
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr ""
@@ -272,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
msgstr ""
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
msgid "Imports an Evernote notebook file (.enex file)."
@@ -413,6 +370,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -423,10 +388,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -545,6 +506,10 @@ msgstr ""
msgid "Evernote Export Files"
msgstr ""
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr ""
@@ -563,6 +528,12 @@ msgstr ""
msgid "Search in all the notes"
msgstr ""
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr ""
@@ -581,6 +552,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -588,12 +562,41 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
#, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr ""
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -650,6 +653,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@@ -685,6 +697,9 @@ msgstr ""
msgid "Set alarm:"
msgstr ""
msgid "Search"
msgstr ""
msgid "Layout"
msgstr ""
@@ -719,6 +734,12 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
msgid "Open..."
msgstr ""
msgid "Save as..."
msgstr ""
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr ""
@@ -726,9 +747,22 @@ msgstr ""
msgid "Attach file"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Set alarm"
msgstr ""
msgid "to-do"
msgstr ""
msgid "note"
msgstr ""
#, javascript-format
msgid "Creating new %s..."
msgstr ""
msgid "Refresh"
msgstr ""
@@ -765,9 +799,6 @@ msgstr ""
msgid "Notebooks"
msgstr ""
msgid "Tags"
msgstr ""
msgid "Searches"
msgstr ""
@@ -785,12 +816,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -917,12 +954,33 @@ msgstr ""
msgid "Dark"
msgstr ""
msgid "Show uncompleted todos on top of the lists"
msgid "Show uncompleted to-dos on top of the lists"
msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -938,9 +996,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +1003,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)"
@@ -960,6 +1015,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1071,6 +1144,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -16,64 +16,12 @@ msgstr ""
"X-Generator: Poedit 2.0.5\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
msgstr "Focus op het volgende paneel"
msgid "Give focus to previous pane"
msgstr "Focus op het vorige paneel"
msgid "Enter command line mode"
msgstr "Ga naar command line modus"
msgid "Exit command line mode"
msgstr "Ga uit command line modus"
msgid "Edit the selected note"
msgstr "Pas de geselecteerde notitie aan"
msgid "Cancel the current command."
msgstr "Annuleer het huidige commando."
msgid "Exit the application."
msgstr "Sluit de applicatie."
msgid "Delete the currently selected note or notebook."
msgstr "Verwijder de geselecteerde notitie of het geselecteerde notitieboek."
msgid "To delete a tag, untag the associated notes."
msgstr "Untag de geassocieerde notities om een tag te verwijderen."
msgid "Please select the note or notebook to be deleted first."
msgstr "Selecteer eerst het notitieboek of de notitie om te verwijderen."
msgid "Set a to-do as completed / not completed"
msgstr "Zet een to-do als voltooid / niet voltooid"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr ""
"Wissel de console tussen gemaximaliseerd/geminimaliseerd/verborgen/zichtbaar."
msgid "Search"
msgstr "Zoeken"
msgid "[t]oggle note [m]etadata."
msgstr "Ac[t]iveer notitie [m]etadata."
msgid "[M]ake a new [n]ote"
msgstr "[M]aak een nieuwe [n]otitie"
msgid "[M]ake a new [t]odo"
msgstr "[M]aak een nieuwe [t]o-do"
msgid "[M]ake a new note[b]ook"
msgstr "[M]aak een nieuw notitie[b]oek"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Kopieer [Y] de [n]otirie in een notitieboek."
msgid "Move the note to a notebook."
msgstr "Verplaats de notitie naar een notitieboek."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Typ Ctrl+D of \"exit\" om de applicatie te sluiten"
@@ -229,6 +177,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Bewerken notitie gestart. Sluit de editor om terug naar de prompt te gaan."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Notitie is opgeslaan."
@@ -255,6 +207,10 @@ msgstr "Toont een geolocatie link voor de notitie."
msgid "Displays usage information."
msgstr "Toont gebruiksinformatie."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Shortcuts zijn niet beschikbaar in command line modus."
@@ -297,8 +253,9 @@ msgstr "Om command line modus te gebruiken, duw \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Om command line modus te verlaten, duw ESCAPE"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Voor de volledige lijst van beschikbare shortcuts, typ `help shortcuts`"
@@ -459,6 +416,14 @@ msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
"Synchroniseer naar opgegeven doel (standaard sync.target configuratie optie)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Synchronisatie reeds bezig."
@@ -472,10 +437,6 @@ msgstr ""
"is, kan de lock file verwijderd worden op \"%s\" en verder gegaan worden met "
"de synchronisatie. "
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisatiedoel: %s (%s)"
@@ -620,6 +581,10 @@ msgstr "Importeer Evernote notities"
msgid "Evernote Export Files"
msgstr "Exporteer Evernote bestanden"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Stop"
@@ -638,6 +603,12 @@ msgstr "Plak"
msgid "Search in all the notes"
msgstr "Zoek in alle notities"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Tools"
@@ -656,6 +627,9 @@ msgstr "Help"
msgid "Website and documentation"
msgstr "Website en documentatie"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Over Joplin"
@@ -663,12 +637,43 @@ msgstr "Over Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Op %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annuleer"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Notities verwijderen?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Annuleer synchronisatie"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notities en instellingen zijn opgeslaan in %s"
@@ -736,6 +741,16 @@ msgstr ""
"(aangeduid met \"active\"). Alle sleutels kunnen gebruikt worden voor "
"decodering, afhankelijk van hoe de notitieboeken initieel versleuteld zijn."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hoofdsleutels"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -773,6 +788,9 @@ msgstr "Hernoem notitieboek:"
msgid "Set alarm:"
msgstr "Stel melding in:"
msgid "Search"
msgstr "Zoeken"
msgid "Layout"
msgstr "Layout"
@@ -809,6 +827,13 @@ msgstr ""
"U heeft momenteel geen notitieboek. Maak een notitieboek door op \"Nieuw "
"notitieboek\" te klikken."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Sla wijzigingen op"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Link of bericht \"%s\" wordt niet ondersteund"
@@ -816,9 +841,24 @@ msgstr "Link of bericht \"%s\" wordt niet ondersteund"
msgid "Attach file"
msgstr "Voeg bestand toe"
msgid "Tags"
msgstr "Tags"
msgid "Set alarm"
msgstr "Zet melding"
#, fuzzy
msgid "to-do"
msgstr "Nieuwe to-do"
#, fuzzy
msgid "note"
msgstr "Nieuwe notitie"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Notities importeren..."
msgid "Refresh"
msgstr "Vernieuwen"
@@ -855,9 +895,6 @@ msgstr "Synchroniseer"
msgid "Notebooks"
msgstr "Notitieboeken"
msgid "Tags"
msgstr "Tags"
msgid "Searches"
msgstr "Zoekopdrachten"
@@ -875,12 +912,19 @@ msgstr "Onbekende optie: %s"
msgid "File system"
msgstr "Bestandssysteem"
#, fuzzy
msgid "Nextcloud"
msgstr "Stel wachtwoord in"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Alleen voor testen)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Onbekend log level: %s"
@@ -1020,12 +1064,36 @@ msgstr "Licht"
msgid "Dark"
msgstr "Donker"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
msgid "Save geo-location with notes"
msgstr "Sla geo-locatie op bij notities"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Maakt nieuwe to-do aan."
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Maakt een nieuwe notitie aan."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Synchronisation interval"
msgstr "Synchronisatie interval"
@@ -1041,9 +1109,6 @@ msgstr "%d uur"
msgid "%d hours"
msgstr "%d uren"
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Show advanced options"
msgstr "Toon geavanceerde opties"
@@ -1051,11 +1116,9 @@ msgid "Synchronisation target"
msgstr "Synchronisatiedoel"
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 ""
"Het doel om mee te synchroniseren. Indien synchroniseren met het "
"bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."
msgid "Directory to synchronise with (absolute path)"
msgstr "Folder om mee te synchroniseren (absolute pad)"
@@ -1067,6 +1130,26 @@ msgstr ""
"Het pad om mee te synchroniseren als bestandssysteem synchronisatie is "
"ingeschakeld. Zie `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
#, fuzzy
msgid "Nexcloud password"
msgstr "Stel wachtwoord in"
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
#, fuzzy
msgid "WebDAV password"
msgstr "Stel wachtwoord in"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1182,6 +1265,12 @@ msgstr "Het notitieboek kon niet opgeslaan worden: %s"
msgid "Edit notebook"
msgstr "Bewerk notitieboek"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Deze notitie werd aangepast:"
@@ -1236,3 +1325,61 @@ msgstr ""
msgid "Welcome"
msgstr "Welkom"
#~ msgid "Give focus to next pane"
#~ msgstr "Focus op het volgende paneel"
#~ msgid "Give focus to previous pane"
#~ msgstr "Focus op het vorige paneel"
#~ msgid "Enter command line mode"
#~ msgstr "Ga naar command line modus"
#~ msgid "Exit command line mode"
#~ msgstr "Ga uit command line modus"
#~ msgid "Edit the selected note"
#~ msgstr "Pas de geselecteerde notitie aan"
#~ msgid "Cancel the current command."
#~ msgstr "Annuleer het huidige commando."
#~ msgid "Exit the application."
#~ msgstr "Sluit de applicatie."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr ""
#~ "Verwijder de geselecteerde notitie of het geselecteerde notitieboek."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Zet een to-do als voltooid / niet voltooid"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "Wissel de console tussen gemaximaliseerd/geminimaliseerd/verborgen/"
#~ "zichtbaar."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "Ac[t]iveer notitie [m]etadata."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "[M]aak een nieuwe [n]otitie"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "[M]aak een nieuwe [t]o-do"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "[M]aak een nieuw notitie[b]oek"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Kopieer [Y] de [n]otirie in een notitieboek."
#~ msgid "Move the note to a notebook."
#~ msgstr "Verplaats de notitie naar een notitieboek."
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Het doel om mee te synchroniseren. Indien synchroniseren met het "
#~ "bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."

View File

@@ -16,63 +16,12 @@ msgstr ""
"X-Generator: Poedit 2.0.5\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Give focus to next pane"
msgstr "Dar o foco para o próximo painel"
msgid "Give focus to previous pane"
msgstr "Dar o foco para o painel anterior"
msgid "Enter command line mode"
msgstr "Entrar no modo de linha de comando"
msgid "Exit command line mode"
msgstr "Sair do modo de linha de comando"
msgid "Edit the selected note"
msgstr "Editar nota selecionada"
msgid "Cancel the current command."
msgstr "Cancelar comando atual."
msgid "Exit the application."
msgstr "Sair da aplicação."
msgid "Delete the currently selected note or notebook."
msgstr "Excluir nota selecionada ou notebook."
msgid "To delete a tag, untag the associated notes."
msgstr "Para eliminar uma tag, remova a tag das notas associadas a ela."
msgid "Please select the note or notebook to be deleted first."
msgstr "Por favor, primeiro, selecione a nota ou caderno a excluir."
msgid "Set a to-do as completed / not completed"
msgstr "Marcar uma tarefa como completada / não completada"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível."
msgid "Search"
msgstr "Procurar"
msgid "[t]oggle note [m]etadata."
msgstr "al[t]ernar [m]etadados de notas."
msgid "[M]ake a new [n]ote"
msgstr "Criar ([M]ake) uma nova [n]ota"
msgid "[M]ake a new [t]odo"
msgstr "Criar ([M]ake) nova [t]arefa"
msgid "[M]ake a new note[b]ook"
msgstr "Criar ([M]ake) novo caderno (note[b]ook)"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "Copiar ([Y]ank) a [n]ota a um caderno."
msgid "Move the note to a notebook."
msgstr "Mover nota para um caderno."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Digite Ctrl+D ou \"exit\" para sair da aplicação"
@@ -223,6 +172,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."
@@ -249,6 +202,10 @@ msgstr "Exibe uma URL de geolocalização para a nota."
msgid "Displays usage information."
msgstr "Exibe informações de uso."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Os atalhos não estão disponíveis no modo CLI."
@@ -290,8 +247,9 @@ msgstr "Para entrar no modo de linha de comando, pressione \":\""
msgid "To exit command line mode, press ESCAPE"
msgstr "Para sair do modo de linha de comando, pressione o ESC"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Para a lista completa de atalhos de teclado disponíveis, digite `help "
"shortcuts`"
@@ -445,6 +403,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."
@@ -458,11 +425,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)"
@@ -596,6 +558,10 @@ msgstr "Importar notas do Evernote"
msgid "Evernote Export Files"
msgstr "Arquivos de Exportação do Evernote"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Sair"
@@ -614,6 +580,12 @@ msgstr "Colar"
msgid "Search in all the notes"
msgstr "Pesquisar em todas as notas"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Ferramentas"
@@ -634,6 +606,9 @@ msgstr "Ajuda"
msgid "Website and documentation"
msgstr "Website e documentação"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Sobre o Joplin"
@@ -641,12 +616,43 @@ msgstr "Sobre o Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Em %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Excluir notas?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Cancelar sincronização"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -705,6 +711,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -741,6 +756,9 @@ msgstr "Renomear caderno:"
msgid "Set alarm:"
msgstr "Definir alarme:"
msgid "Search"
msgstr "Procurar"
msgid "Layout"
msgstr "Layout"
@@ -778,6 +796,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "Atualmente, não há notas. Crie uma, clicando no botão (+)."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Gravar alterações"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Link ou mensagem não suportada: %s"
@@ -785,9 +810,24 @@ msgstr "Link ou mensagem não suportada: %s"
msgid "Attach file"
msgstr "Anexar arquivo"
msgid "Tags"
msgstr "Tags"
msgid "Set alarm"
msgstr "Definir alarme"
#, fuzzy
msgid "to-do"
msgstr "Nova tarefa"
#, fuzzy
msgid "note"
msgstr "Nova nota"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Importando notas ..."
msgid "Refresh"
msgstr "Atualizar"
@@ -825,9 +865,6 @@ msgstr "Sincronizar"
msgid "Notebooks"
msgstr "Cadernos"
msgid "Tags"
msgstr "Tags"
msgid "Searches"
msgstr "Pesquisas"
@@ -846,12 +883,18 @@ msgstr "Flag desconhecido: %s"
msgid "File system"
msgstr "Sistema de arquivos"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (apenas para testes)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nível de log desconhecido: %s"
@@ -990,12 +1033,37 @@ msgstr "Light"
msgid "Dark"
msgstr "Dark"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Mostrar tarefas incompletas no topo das listas"
msgid "Save geo-location with notes"
msgstr "Salvar geolocalização com notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Cria uma nova tarefa."
#, fuzzy
msgid "Focus title"
msgstr "Título da nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Cria uma nova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronização"
@@ -1011,9 +1079,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"
@@ -1021,11 +1086,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 ""
@@ -1037,6 +1100,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV 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."
@@ -1149,6 +1230,12 @@ msgstr "O caderno não pôde ser salvo: %s"
msgid "Edit notebook"
msgstr "Editar caderno"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota foi modificada:"
@@ -1202,8 +1289,61 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome"
msgstr "Bem-vindo"
#~ msgid "Note title:"
#~ msgstr "Título da nota:"
#~ msgid "Give focus to next pane"
#~ msgstr "Dar o foco para o próximo painel"
#~ msgid "Give focus to previous pane"
#~ msgstr "Dar o foco para o painel anterior"
#~ msgid "Enter command line mode"
#~ msgstr "Entrar no modo de linha de comando"
#~ msgid "Exit command line mode"
#~ msgstr "Sair do modo de linha de comando"
#~ msgid "Edit the selected note"
#~ msgstr "Editar nota selecionada"
#~ msgid "Cancel the current command."
#~ msgstr "Cancelar comando atual."
#~ msgid "Exit the application."
#~ msgstr "Sair da aplicação."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Excluir nota selecionada ou notebook."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Marcar uma tarefa como completada / não completada"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "al[t]ernar [m]etadados de notas."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "Criar ([M]ake) uma nova [n]ota"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "Criar ([M]ake) nova [t]arefa"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "Criar ([M]ake) novo caderno (note[b]ook)"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "Copiar ([Y]ank) a [n]ota a um caderno."
#~ msgid "Move the note to a notebook."
#~ msgstr "Mover nota para um caderno."
#~ 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 "To-do title:"
#~ msgstr "Título da tarefa:"

View File

@@ -7,73 +7,22 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: rtmkrlv <artyom.karlov@gmail.com>\n"
"Last-Translator: Artyom Karlov <artyom.karlov@gmail.com>\n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\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"
msgid "Give focus to next pane"
msgstr "Переключиться на следующую панель"
msgid "Give focus to previous pane"
msgstr "Переключиться на предыдущую панель"
msgid "Enter command line mode"
msgstr "Войти в режим командной строки"
msgid "Exit command line mode"
msgstr "Выйти из режима командной строки"
msgid "Edit the selected note"
msgstr "Редактировать выбранную заметку"
msgid "Cancel the current command."
msgstr "Отменить текущую команду."
msgid "Exit the application."
msgstr "Выйти из приложения."
msgid "Delete the currently selected note or notebook."
msgstr "Удалить текущую выбранную заметку или блокнот."
msgid "To delete a tag, untag the associated notes."
msgstr "Чтобы удалить тег, уберите его с ассоциированных с ним заметок."
msgid "Please select the note or notebook to be deleted first."
msgstr "Сначала выберите заметку или блокнот, которые должны быть удалены."
msgid "Set a to-do as completed / not completed"
msgstr "Отметить задачу как завершённую/незавершённую"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "[tc] переключить консоль между развёрнутой/свёрнутой/скрытой/видимой."
msgid "Search"
msgstr "Поиск"
msgid "[t]oggle note [m]etadata."
msgstr "[tm] переключить отображение метаданных заметки."
msgid "[M]ake a new [n]ote"
msgstr "[mn] создать новую заметку"
msgid "[M]ake a new [t]odo"
msgstr "[mt] создать новую задачу"
msgid "[M]ake a new note[b]ook"
msgstr "[mb] создать новый блокнот"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "[yn] копировать заметку в блокнот."
msgid "Move the note to a notebook."
msgstr "Переместить заметку в блокнот."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Для выхода из приложения нажмите Ctrl+D или введите «exit»"
@@ -112,7 +61,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Команда «%s» доступна только в режиме GUI"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Не удалось изменить зашифрованный элемент"
#, javascript-format
msgid "Missing required argument: %s"
@@ -123,7 +72,7 @@ msgid "%s: %s"
msgstr "%s: %s"
msgid "Your choice: "
msgstr "Ваш выбор:"
msgstr "Ваш выбор: "
#, javascript-format
msgid "Invalid answer: %s"
@@ -180,22 +129,24 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
"`status` и `target-status`."
#, fuzzy
msgid "Enter master password:"
msgstr "Установить пароль"
msgstr "Введите мастер-пароль:"
msgid "Operation cancelled"
msgstr ""
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 "Включить шифрование"
msgstr "Расшифровка завершена."
msgid "Enabled"
msgstr "Включено"
@@ -203,9 +154,9 @@ msgstr "Включено"
msgid "Disabled"
msgstr "Отключено"
#, fuzzy, javascript-format
#, javascript-format
msgid "Encryption is: %s"
msgstr "Шифрование:"
msgstr "Шифрование: %s"
msgid "Edit note."
msgstr "Редактировать заметку."
@@ -228,6 +179,10 @@ msgstr ""
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
"командной строке."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Ошибка при открытии заметки в редакторе: %s"
msgid "Note has been saved."
msgstr "Заметка сохранена."
@@ -253,6 +208,10 @@ msgstr "Выводит URL геолокации для заметки."
msgid "Displays usage information."
msgstr "Выводит информацию об использовании."
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "Ярлыки недоступны в режиме командной строки."
@@ -277,7 +236,7 @@ msgstr ""
"элемент."
msgid "To move from one pane to another, press Tab or Shift+Tab."
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab"
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab."
msgid ""
"Use the arrows and page up/down to scroll the lists and text areas "
@@ -295,11 +254,12 @@ msgstr "Чтобы войти в режим командной строки, н
msgid "To exit command line mode, press ESCAPE"
msgstr "Чтобы выйти из режима командной строки, нажмите ESCAPE"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr ""
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
"shortcuts`."
"shortcuts`"
msgid "Imports an Evernote notebook file (.enex file)."
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
@@ -455,6 +415,15 @@ 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 ""
"Не аутентифицировано с %s. Пожалуйста, предоставьте все недостающие данные."
msgid "Synchronisation is already in progress."
msgstr "Синхронизация уже выполняется."
@@ -468,10 +437,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)"
@@ -593,6 +558,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
"расшифруются в фоновом режиме и вскоре станут доступны."
msgid "File"
msgstr "Файл"
@@ -612,6 +581,10 @@ msgstr "Импортировать заметки из Evernote"
msgid "Evernote Export Files"
msgstr "Файлы экспорта Evernote"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "Выход"
@@ -630,19 +603,23 @@ msgstr "Вставить"
msgid "Search in all the notes"
msgstr "Поиск во всех заметках"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "Инструменты"
msgid "Synchronisation status"
msgstr "Статус синхронизации"
#, fuzzy
msgid "Encryption options"
msgstr "Настройки шифрования"
#, fuzzy
msgid "General Options"
msgstr "Настройки"
msgstr "Основные настройки"
msgid "Help"
msgstr "Помощь"
@@ -650,6 +627,9 @@ msgstr "Помощь"
msgid "Website and documentation"
msgstr "Сайт и документация"
msgid "Check for updates..."
msgstr "Проверить обновления..."
msgid "About Joplin"
msgstr "О Joplin"
@@ -657,12 +637,44 @@ msgstr "О Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "В %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Отмена"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "Удалить заметки?"
#, fuzzy
msgid "An update is available, do you want to download it now?"
msgstr "Доступно обновление. Обновить сейчас?"
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
msgid "Current version is up-to-date."
msgstr "Вы используете самую свежую версию."
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Отменить синхронизацию"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Заметки и настройки сохранены в: %s"
@@ -731,6 +743,16 @@ msgstr ""
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
"блокноты."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Мастер-ключи"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Статус"
@@ -766,6 +788,9 @@ msgstr "Переименовать блокнот:"
msgid "Set alarm:"
msgstr "Установить напоминание:"
msgid "Search"
msgstr "Поиск"
msgid "Layout"
msgstr "Вид"
@@ -800,6 +825,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "Сейчас здесь нет блокнотов. Создайте новый нажав «Новый блокнот»."
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "Сохранить изменения"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "Неподдерживаемая ссыка или сообщение: %s"
@@ -807,9 +839,24 @@ msgstr "Неподдерживаемая ссыка или сообщение: %
msgid "Attach file"
msgstr "Прикрепить файл"
msgid "Tags"
msgstr "Теги"
msgid "Set alarm"
msgstr "Установить напоминание"
#, fuzzy
msgid "to-do"
msgstr "Новая задача"
#, fuzzy
msgid "note"
msgstr "Новая заметка"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "Импорт заметок..."
msgid "Refresh"
msgstr "Обновить"
@@ -846,9 +893,6 @@ msgstr "Синхронизировать"
msgid "Notebooks"
msgstr "Блокноты"
msgid "Tags"
msgstr "Теги"
msgid "Searches"
msgstr "Запросы"
@@ -866,12 +910,20 @@ msgstr "Неизвестный флаг: %s"
msgid "File system"
msgstr "Файловая система"
#, fuzzy
msgid "Nextcloud"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (только для тестирования)"
#, fuzzy
msgid "WebDAV"
msgstr "Nexcloud WebDAV URL"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Неизвестный уровень лога: %s"
@@ -930,9 +982,9 @@ msgstr "Удалено локальных элементов: %d."
msgid "Deleted remote items: %d."
msgstr "Удалено удалённых элементов: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Создано локальных элементов: %d."
msgstr "Получено элементов: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
@@ -949,13 +1001,11 @@ msgstr "Завершено: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s"
#, fuzzy
msgid "Encrypted"
msgstr "Шифрование:"
msgstr "Зашифровано"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Некоторые элементы не могут быть синхронизированы."
msgstr "Зашифрованные элементы не могут быть изменены"
msgid "Conflicts"
msgstr "Конфликты"
@@ -1010,12 +1060,34 @@ msgstr "Светлая"
msgid "Dark"
msgstr "Тёмная"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "Показывать незавершённые задачи вверху списков"
msgid "Save geo-location with notes"
msgstr "Сохранять информацию о геолокации в заметках"
msgid "When creating a new to-do:"
msgstr "При создании новой задачи:"
msgid "Focus title"
msgstr "Фокус на названии"
msgid "Focus body"
msgstr "Фокус на содержимом"
msgid "When creating a new note:"
msgstr "При создании новой заметки:"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Масштаб приложения в процентах"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Synchronisation interval"
msgstr "Интервал синхронизации"
@@ -1031,9 +1103,6 @@ msgstr "%d час"
msgid "%d hours"
msgstr "%d часов"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Show advanced options"
msgstr "Показывать расширенные настройки"
@@ -1041,11 +1110,11 @@ 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` указывается целевой каталог."
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
"параметры, именованные как «sync.NUM.NAME» (все описаны ниже)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"
@@ -1057,6 +1126,27 @@ msgstr ""
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Имя пользователя Nexcloud"
msgid "Nexcloud password"
msgstr "Пароль Nexcloud"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Имя пользователя Nexcloud"
#, fuzzy
msgid "WebDAV password"
msgstr "Установить пароль"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
@@ -1064,15 +1154,18 @@ msgstr "Неверное значение параметра: «%s». Досту
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%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 ""
"Эти элементы будут оставаться на устройстве, но не будут загружены в целевой "
"объект синхронизации. Чтобы найти эти элементы, воспользуйтесь поиском по "
"названию или ID (который указывается в скобках выше)."
msgid "Sync status (synced items / total items)"
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
@@ -1119,9 +1212,8 @@ msgstr "Лог"
msgid "Export Debug Report"
msgstr "Экспортировать отладочный отчёт"
#, fuzzy
msgid "Encryption Config"
msgstr "Шифрование:"
msgstr "Конфигурация шифрования"
msgid "Configuration"
msgstr "Конфигурация"
@@ -1134,7 +1226,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "Переместить %d заметок в блокнот «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Нажмите, чтобы установить пароль для расшифровки."
msgid "Select date"
msgstr "Выбрать дату"
@@ -1145,22 +1237,20 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию"
#, fuzzy, javascript-format
#, javascript-format
msgid "Master Key %s"
msgstr "Мастер-ключи"
msgstr "Мастер-ключ %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Создано: %d."
msgstr "Создано: %s"
#, fuzzy
msgid "Password:"
msgstr "Пароль"
msgstr "Пароль:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Пароль не может быть пустым"
#, fuzzy
msgid "Enable"
msgstr "Включено"
@@ -1171,6 +1261,13 @@ msgstr "Не удалось сохранить блокнот: %s"
msgid "Edit notebook"
msgstr "Редактировать блокнот"
msgid "Show all"
msgstr ""
#, fuzzy
msgid "Errors only"
msgstr "Ошибка"
msgid "This note has been modified:"
msgstr "Эта заметка была изменена:"
@@ -1224,8 +1321,74 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome"
msgstr "Добро пожаловать"
#~ msgid "Note title:"
#~ msgstr "Название заметки:"
#~ msgid "Give focus to next pane"
#~ msgstr "Переключиться на следующую панель"
#~ msgid "Give focus to previous pane"
#~ msgstr "Переключиться на предыдущую панель"
#~ msgid "Enter command line mode"
#~ msgstr "Войти в режим командной строки"
#~ msgid "Exit command line mode"
#~ msgstr "Выйти из режима командной строки"
#~ msgid "Edit the selected note"
#~ msgstr "Редактировать выбранную заметку"
#~ msgid "Cancel the current command."
#~ msgstr "Отменить текущую команду."
#~ msgid "Exit the application."
#~ msgstr "Выйти из приложения."
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "Удалить текущую выбранную заметку или блокнот."
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "Отметить задачу как завершённую/незавершённую"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr ""
#~ "[tc] переключить консоль между развёрнутой/свёрнутой/скрытой/видимой."
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "[tm] переключить отображение метаданных заметки."
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "[mn] создать новую заметку"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "[mt] создать новую задачу"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "[mb] создать новый блокнот"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "[yn] копировать заметку в блокнот."
#~ msgid "Move the note to a notebook."
#~ msgstr "Переместить заметку в блокнот."
#~ msgid "Error"
#~ msgstr "Ошибка"
#~ msgid "Could not download the update: %s"
#~ msgstr "Не удалось загрузить обновление: %s"
#~ msgid "New version downloaded - application will quit now and update..."
#~ msgstr ""
#~ "Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
#~ msgid "Could not install the update: %s"
#~ msgstr "Не удалось установить обновление: %s"
#~ 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 "To-do title:"
#~ msgstr "Название задачи:"

View File

@@ -15,63 +15,12 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Give focus to next pane"
msgstr "聚焦于下个面板"
msgid "Give focus to previous pane"
msgstr "聚焦于上个面板"
msgid "Enter command line mode"
msgstr "进入命令行模式"
msgid "Exit command line mode"
msgstr "退出命令行模式"
msgid "Edit the selected note"
msgstr "编辑所选笔记"
msgid "Cancel the current command."
msgstr "取消当前命令。"
msgid "Exit the application."
msgstr "退出程序。"
msgid "Delete the currently selected note or notebook."
msgstr "删除当前所选笔记或笔记本。"
msgid "To delete a tag, untag the associated notes."
msgstr "移除相关笔记的标签后才可删除此标签。"
msgid "Please select the note or notebook to be deleted first."
msgstr "请选择最先删除的笔记或笔记本。"
msgid "Set a to-do as completed / not completed"
msgstr "设置待办事项为已完成或未完成"
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
msgstr "在最大化/最小化/隐藏/显示间切换[t]控制台[c]。"
msgid "Search"
msgstr "搜索"
msgid "[t]oggle note [m]etadata."
msgstr "切换[t]笔记元数据[m]。"
msgid "[M]ake a new [n]ote"
msgstr "创建[M]新笔记[n]"
msgid "[M]ake a new [t]odo"
msgstr "创建[M]新待办事项[t]"
msgid "[M]ake a new note[b]ook"
msgstr "创建[M]新笔记本[b]"
msgid "Copy ([Y]ank) the [n]ote to a notebook."
msgstr "复制[Y]笔记[n]至笔记本。"
msgid "Move the note to a notebook."
msgstr "移动笔记至笔记本。"
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "按Ctrl+D或输入\"exit\"退出程序"
@@ -218,6 +167,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 "笔记已被保存。"
@@ -243,6 +196,10 @@ msgstr "显示此笔记的地理定位URL地址。"
msgid "Displays usage information."
msgstr "显示使用信息。"
#, javascript-format
msgid "For information on how to customise the shortcuts please visit %s"
msgstr ""
msgid "Shortcuts are not available in CLI mode."
msgstr "快捷键在CLI模式下不可用。"
@@ -280,8 +237,9 @@ msgstr "按\":\"键进入命令行模式"
msgid "To exit command line mode, press ESCAPE"
msgstr "按ESC键退出命令行模式"
#, fuzzy
msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
"For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr "输入`help shortcuts`显示全部可用的快捷键列表。"
msgid "Imports an Evernote notebook file (.enex file)."
@@ -426,6 +384,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 "同步正在进行中。"
@@ -438,10 +404,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)"
@@ -568,6 +530,10 @@ msgstr "导入Evernote笔记"
msgid "Evernote Export Files"
msgstr "Evernote导出文件"
#, javascript-format
msgid "Hide %s"
msgstr ""
msgid "Quit"
msgstr "退出"
@@ -586,6 +552,12 @@ msgstr "粘贴"
msgid "Search in all the notes"
msgstr "在所有笔记内搜索"
msgid "View"
msgstr ""
msgid "Toggle editor layout"
msgstr ""
msgid "Tools"
msgstr "工具"
@@ -605,6 +577,9 @@ msgstr "帮助"
msgid "Website and documentation"
msgstr "网站与文档"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "关于Joplin"
@@ -612,12 +587,43 @@ msgstr "关于Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "%s:%s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "确认"
msgid "Cancel"
msgstr "取消"
#, fuzzy, javascript-format
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "是否删除笔记?"
msgid "An update is available, do you want to download it now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "否"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "取消同步"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -676,6 +682,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状态"
@@ -711,6 +726,9 @@ msgstr "重命名笔记本:"
msgid "Set alarm:"
msgstr "设置提醒:"
msgid "Search"
msgstr "搜索"
msgid "Layout"
msgstr "布局"
@@ -747,6 +765,13 @@ msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "当前无笔记。点击(+)创建新笔记。"
msgid "Open..."
msgstr ""
#, fuzzy
msgid "Save as..."
msgstr "保存更改"
#, javascript-format
msgid "Unsupported link or message: %s"
msgstr "不支持的链接或信息:%s"
@@ -754,9 +779,24 @@ msgstr "不支持的链接或信息:%s"
msgid "Attach file"
msgstr "附加文件"
msgid "Tags"
msgstr "标签"
msgid "Set alarm"
msgstr "设置提醒"
#, fuzzy
msgid "to-do"
msgstr "新待办事项"
#, fuzzy
msgid "note"
msgstr "新笔记"
#, fuzzy, javascript-format
msgid "Creating new %s..."
msgstr "正在导入笔记..."
msgid "Refresh"
msgstr "刷新"
@@ -793,9 +833,6 @@ msgstr "同步"
msgid "Notebooks"
msgstr "笔记本"
msgid "Tags"
msgstr "标签"
msgid "Searches"
msgstr "搜索历史"
@@ -814,12 +851,18 @@ msgstr "未知标记:%s"
msgid "File system"
msgstr "文件系统"
msgid "Nextcloud"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive开发员(仅测试用)"
msgid "WebDAV"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "未知日志level:%s"
@@ -952,12 +995,37 @@ msgstr "浅色"
msgid "Dark"
msgstr "深色"
msgid "Show uncompleted todos on top of the lists"
#, fuzzy
msgid "Show uncompleted to-dos on top of the lists"
msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "创建新待办事项。"
#, fuzzy
msgid "Focus title"
msgstr "笔记标题:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "创建新笔记。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Synchronisation interval"
msgstr "同步间隔"
@@ -973,9 +1041,6 @@ msgstr "%d小时"
msgid "%d hours"
msgstr "%d小时"
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Show advanced options"
msgstr "显示高级选项"
@@ -983,9 +1048,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 ""
@@ -995,6 +1060,24 @@ msgid ""
"See `sync.target`."
msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
@@ -1107,6 +1190,12 @@ msgstr "此笔记本无法保存:%s"
msgid "Edit notebook"
msgstr "编辑笔记本"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "此笔记已被修改:"
@@ -1158,8 +1247,58 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
msgid "Welcome"
msgstr "欢迎"
#~ msgid "Note title:"
#~ msgstr "笔记标题:"
#~ msgid "Give focus to next pane"
#~ msgstr "聚焦于下个面板"
#~ msgid "Give focus to previous pane"
#~ msgstr "聚焦于上个面板"
#~ msgid "Enter command line mode"
#~ msgstr "进入命令行模式"
#~ msgid "Exit command line mode"
#~ msgstr "退出命令行模式"
#~ msgid "Edit the selected note"
#~ msgstr "编辑所选笔记"
#~ msgid "Cancel the current command."
#~ msgstr "取消当前命令。"
#~ msgid "Exit the application."
#~ msgstr "退出程序。"
#~ msgid "Delete the currently selected note or notebook."
#~ msgstr "删除当前所选笔记或笔记本。"
#~ msgid "Set a to-do as completed / not completed"
#~ msgstr "设置待办事项为已完成或未完成"
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
#~ msgstr "在最大化/最小化/隐藏/显示间切换[t]控制台[c]。"
#~ msgid "[t]oggle note [m]etadata."
#~ msgstr "切换[t]笔记元数据[m]。"
#~ msgid "[M]ake a new [n]ote"
#~ msgstr "创建[M]新笔记[n]"
#~ msgid "[M]ake a new [t]odo"
#~ msgstr "创建[M]新待办事项[t]"
#~ msgid "[M]ake a new note[b]ook"
#~ msgstr "创建[M]新笔记本[b]"
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
#~ msgstr "复制[Y]笔记[n]至笔记本。"
#~ msgid "Move the note to a notebook."
#~ 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 "To-do title:"
#~ msgstr "待办事项标题:"

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "0.10.88",
"version": "1.0.98",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -64,6 +64,11 @@
}
}
},
"async-mutex": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -85,6 +90,11 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
@@ -1050,6 +1060,11 @@
"strict-uri-encode": "1.1.0"
}
},
"querystringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@@ -1104,6 +1119,11 @@
"uuid": "3.1.0"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
@@ -2037,9 +2057,9 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"tkwidgets": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.21.tgz",
"integrity": "sha512-gJfpYq3UM6AZ23ZM+D9BZ1PhsJLLHgjCOf487/lS9pO0uDdnkMcVXkkKEfRl00EyjPnGc88QZhEkVOvrtKsuPA==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.25.tgz",
"integrity": "sha512-f+12QbxNCLg9Jou5JoPJxATGLmzpDAQeM7QRTXvuqdEB/QvPD9+UlPUL7eYJP1QJv2zzT6EIWWbdpDkXPEtzCQ==",
"requires": {
"chalk": "2.3.0",
"emphasize": "1.5.0",
@@ -2100,6 +2120,15 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
@@ -2144,6 +2173,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
}
},
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
},
"xregexp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.88",
"version": "1.0.98",
"bin": {
"joplin": "./main.js"
},
@@ -28,6 +28,8 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
@@ -56,9 +58,11 @@
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.21",
"tkwidgets": "^0.5.25",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {

View File

@@ -11,7 +11,7 @@ cp "$SCRIPT_DIR/../README.md" build/
cd "$SCRIPT_DIR/build"
npm publish
NEW_VERSION=$("cat package.json | jq -r .version")
NEW_VERSION=$(cat package.json | jq -r .version)
git add -A
git commit -m "CLI v$NEW_VERSION"
git tag "cli-v$NEW_VERSION"

View File

@@ -9,9 +9,7 @@ rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data"
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)
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js)
else
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi

View File

@@ -8,7 +8,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('Encryption', function() {
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
@@ -29,4 +29,19 @@ describe('Encryption', function() {
done();
});
it('should find items using binary search', async (done) => {
let items = ['aaa', 'ccc', 'bbb'];
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
items.sort();
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(1);
expect(ArrayUtils.binarySearch(items, 'ccc')).toBe(2);
expect(ArrayUtils.binarySearch(items, 'oops')).toBe(-1);
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(0);
items = [];
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
done();
});
});

View File

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

View File

@@ -19,7 +19,7 @@ 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
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + 30000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@@ -60,6 +60,7 @@ async function allSyncTargetItemsEncrypted() {
}
async function localItemsSameAsRemote(locals, expect) {
let error = null;
try {
let files = await fileApi().list();
files = files.items;
@@ -74,19 +75,22 @@ async function localItemsSameAsRemote(locals, expect) {
expect(!!remote).toBe(true);
if (!remote) continue;
if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
} else {
expect(remote.updated_time).toBe(dbItem.updated_time);
}
// if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
// expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
// } else {
// expect(remote.updated_time).toBe(dbItem.updated_time);
// }
let remoteContent = await fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
expect(remoteContent.title).toBe(dbItem.title);
}
} catch (error) {
console.error(error);
} catch (e) {
error = e;
}
expect(error).toBe(null);
}
let insideBeforeEach = false;
@@ -304,7 +308,7 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
}));
it('should delete remote folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -322,8 +326,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
localItemsSameAsRemote(all, expect);
}));
await localItemsSameAsRemote(all, expect);
}));
it('should delete local folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -345,8 +349,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items = await allItems();
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
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" });
@@ -388,8 +392,8 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
it('should cross delete all folders', asyncTest(async () => {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
@@ -459,7 +463,7 @@ describe('Synchronizer', function() {
let unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
}));
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -489,7 +493,7 @@ describe('Synchronizer', function() {
let items = await allItems();
expect(items.length).toBe(1);
}));
}));
it('should allow duplicate folder titles', asyncTest(async () => {
let localF1 = await Folder.save({ title: "folder" });
@@ -575,10 +579,12 @@ describe('Synchronizer', function() {
}
it('should sync tags', asyncTest(async () => {
await shoudSyncTagTest(false); }));
await shoudSyncTagTest(false);
}));
it('should sync encrypted tags', asyncTest(async () => {
await shoudSyncTagTest(true); }));
await shoudSyncTagTest(true);
}));
it('should not sync notes with conflicts', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });
@@ -683,12 +689,12 @@ describe('Synchronizer', function() {
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);
@@ -702,9 +708,9 @@ describe('Synchronizer', function() {
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['rejectedByTarget'];
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);
@@ -848,7 +854,7 @@ describe('Synchronizer', function() {
}));
it('should sync resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
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 });
@@ -983,4 +989,14 @@ describe('Synchronizer', function() {
expect(resource1.encryption_blob_encrypted).toBe(0);
}));
it('should create remote items with UTF-8 content', asyncTest(async () => {
let folder = await Folder.save({ title: "Fahrräder" });
await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id });
let all = await allItems();
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
}));
});

View File

@@ -15,6 +15,7 @@ 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');
@@ -22,8 +23,10 @@ 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_ = [];
@@ -46,12 +49,16 @@ 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 syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;//400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const logger = new Logger();
logger.addTarget('console');
@@ -68,6 +75,8 @@ BaseItem.loadClass('MasterKey', MasterKey);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');
Setting.autoSaveEnabled = false;
function syncTargetId() {
return syncTargetId_;
}
@@ -174,12 +183,7 @@ async function setupDatabaseAndSynchronizer(id = null) {
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
} else {
await fileApi().format();
}
await fileApi().clearRoot();
}
function db(id = null) {
@@ -230,7 +234,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) {
@@ -250,6 +264,7 @@ function fileApi() {
fileApi_.setLogger(logger);
fileApi_.setSyncTargetId(syncTargetId_);
fileApi_.requestRepeatCount_ = 0;
return fileApi_;
}

View File

@@ -1,9 +1,11 @@
const { _ } = require('lib/locale.js');
const { BrowserWindow } = require('electron');
const { BrowserWindow, Menu, Tray } = require('electron');
const { shim } = require('lib/shim');
const url = require('url')
const path = require('path')
const urlUtils = require('lib/urlUtils.js');
const { dirname, basename } = require('lib/path-utils');
const fs = require('fs-extra');
class ElectronAppWrapper {
@@ -12,6 +14,8 @@ class ElectronAppWrapper {
this.env_ = env;
this.win_ = null;
this.willQuitApp_ = false;
this.tray_ = null;
this.buildDir_ = null;
}
electronApp() {
@@ -59,14 +63,30 @@ class ElectronAppWrapper {
}))
// Uncomment this to view errors if the application does not start
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
if (this.willQuitApp_ || process.platform !== 'darwin') {
this.win_ = null;
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
// user clicks on the icon in the task bar).
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
// case the app must be explicitely closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
if (process.platform === 'darwin') {
if (this.willQuitApp_) {
this.win_ = null;
} else {
event.preventDefault();
this.win_.hide();
}
} else {
event.preventDefault();
this.win_.hide();
if (this.trayShown() && !this.willQuitApp_) {
event.preventDefault();
this.win_.hide();
} else {
this.win_ = null;
}
}
})
@@ -93,6 +113,43 @@ class ElectronAppWrapper {
this.electronApp_.quit();
}
trayShown() {
return !!this.tray_;
}
buildDir() {
if (this.buildDir_) return this.buildDir_;
let dir = __dirname + '/build';
if (!fs.pathExistsSync(dir)) {
dir = dirname(__dirname) + '/build';
if (!fs.pathExistsSync(dir)) throw new Error('Cannot find build dir');
}
this.buildDir_ = dir;
return dir;
}
// Note: this must be called only after the "ready" event of the app has been dispatched
createTray(contextMenu) {
try {
this.tray_ = new Tray(this.buildDir() + '/icons/16x16.png')
this.tray_.setToolTip(this.electronApp_.getName())
this.tray_.setContextMenu(contextMenu)
this.tray_.on('click', () => {
this.window().show();
});
} catch (error) {
console.error("Cannot create tray", error);
}
}
destroyTray() {
if (!this.tray_) return;
this.tray_.destroy();
this.tray_ = null;
}
async start() {
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is ready.

View File

@@ -49,6 +49,10 @@ class Application extends BaseApplication {
return true;
}
checkForUpdateLoggerPath() {
return Setting.value('profileDir') + '/log-autoupdater.txt';
}
reducer(state = appDefaultState, action) {
let newState = state;
@@ -136,6 +140,10 @@ class Application extends BaseApplication {
this.refreshMenu();
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
this.updateTray();
}
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
}
@@ -192,6 +200,7 @@ class Application extends BaseApplication {
}
}, {
label: _('New notebook'),
accelerator: 'CommandOrControl+B',
screens: ['Main'],
click: () => {
this.dispatch({
@@ -220,6 +229,14 @@ class Application extends BaseApplication {
},
});
}
}, {
type: 'separator',
platforms: ['darwin'],
}, {
label: _('Hide %s', 'Joplin'),
platforms: ['darwin'],
accelerator: 'CommandOrControl+H',
click: () => { bridge().window().hide() }
}, {
type: 'separator',
}, {
@@ -231,17 +248,17 @@ class Application extends BaseApplication {
label: _('Edit'),
submenu: [{
label: _('Copy'),
screens: ['Main', 'OneDriveLogin'],
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
role: 'copy',
accelerator: 'CommandOrControl+C',
}, {
label: _('Cut'),
screens: ['Main', 'OneDriveLogin'],
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
role: 'cut',
accelerator: 'CommandOrControl+X',
}, {
label: _('Paste'),
screens: ['Main', 'OneDriveLogin'],
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
role: 'paste',
accelerator: 'CommandOrControl+V',
}, {
@@ -258,6 +275,19 @@ class Application extends BaseApplication {
});
},
}],
}, {
label: _('View'),
submenu: [{
label: _('Toggle editor layout'),
screens: ['Main'],
accelerator: 'CommandOrControl+L',
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'toggleVisiblePanes',
});
}
}],
}, {
label: _('Tools'),
submenu: [{
@@ -281,6 +311,7 @@ class Application extends BaseApplication {
}
},{
label: _('General Options'),
accelerator: 'CommandOrControl+,',
click: () => {
this.dispatch({
type: 'NAV_GO',
@@ -294,6 +325,11 @@ class Application extends BaseApplication {
label: _('Website and documentation'),
accelerator: 'F1',
click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('Check for updates...'),
click: () => {
bridge().checkForUpdates(false, bridge().window(), this.checkForUpdateLoggerPath());
}
}, {
label: _('About Joplin'),
click: () => {
@@ -321,10 +357,13 @@ class Application extends BaseApplication {
}
function removeUnwantedItems(template, screen) {
const platform = shim.platformName();
let output = [];
for (let i = 0; i < template.length; i++) {
const t = Object.assign({}, template[i]);
if (t.screens && t.screens.indexOf(screen) < 0) continue;
if (t.platforms && t.platforms.indexOf(platform) < 0) continue;
if (t.submenu) t.submenu = removeUnwantedItems(t.submenu, screen);
if (('submenu' in t) && isEmptyMenu(t.submenu)) continue;
output.push(t);
@@ -340,6 +379,28 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = screen;
}
updateTray() {
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
// Might be fixed in Electron 18.x but no non-beta release yet.
if (!shim.isWindows() && !shim.isMac()) return;
const app = bridge().electronApp();
if (app.trayShown() === Setting.value('showTrayIcon')) return;
if (!Setting.value('showTrayIcon')) {
app.destroyTray();
} else {
const contextMenu = Menu.buildFromTemplate([
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
{ type: 'separator' },
{ label: _('Exit'), click: () => { app.exit() } },
])
app.createTray(contextMenu);
}
}
async start(argv) {
argv = await super.start(argv);
@@ -385,17 +446,20 @@ class Application extends BaseApplication {
// Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = function() {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath());
}
}
// Initial check on startup
setTimeout(() => { runAutoUpdateCheck() }, 5000);
// For those who leave the app always open
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
// Then every x hours
setInterval(() => { runAutoUpdateCheck() }, 12 * 60 * 60 * 1000);
}
this.updateTray();
setTimeout(() => {
AlarmService.garbageCollect();
}, 1000 * 60 * 60);

View File

@@ -43,7 +43,7 @@ class Bridge {
const {dialog} = require('electron');
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
const filePath = dialog.showSaveDialog(options);
const filePath = dialog.showSaveDialog(this.window(), options);
if (filePath) {
this.lastSelectedPath_ = filePath;
}
@@ -55,27 +55,27 @@ class Bridge {
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
if (!('createDirectory' in options)) options.createDirectory = true;
const filePaths = dialog.showOpenDialog(options);
const filePaths = dialog.showOpenDialog(this.window(), options);
if (filePaths && filePaths.length) {
this.lastSelectedPath_ = dirname(filePaths[0]);
}
return filePaths;
}
showMessageBox(options) {
showMessageBox(window, options) {
const {dialog} = require('electron');
return dialog.showMessageBox(options);
return dialog.showMessageBox(window, options);
}
showErrorMessageBox(message) {
return this.showMessageBox({
return this.showMessageBox(this.window(), {
type: 'error',
message: message,
});
}
showConfirmMessageBox(message) {
const result = this.showMessageBox({
const result = this.showMessageBox(this.window(), {
type: 'question',
message: message,
buttons: [_('OK'), _('Cancel')],
@@ -84,7 +84,7 @@ class Bridge {
}
showInfoMessageBox(message) {
const result = this.showMessageBox({
const result = this.showMessageBox(this.window(), {
type: 'info',
message: message,
buttons: [_('OK')],
@@ -108,21 +108,26 @@ class Bridge {
return require('electron').shell.openItem(fullPath)
}
async checkForUpdatesAndNotify(logFilePath) {
if (!this.autoUpdater_) {
this.autoUpdateLogger_ = new Logger();
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
this.autoUpdater_ = require("electron-updater").autoUpdater;
this.autoUpdater_.logger = this.autoUpdateLogger_;
}
// async checkForUpdatesAndNotify(logFilePath) {
// if (!this.autoUpdater_) {
// this.autoUpdateLogger_ = new Logger();
// this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
// this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
// this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
// this.autoUpdater_ = require("electron-updater").autoUpdater;
// this.autoUpdater_.logger = this.autoUpdateLogger_;
// }
try {
await this.autoUpdater_.checkForUpdatesAndNotify();
} catch (error) {
this.autoUpdateLogger_.error(error);
}
// try {
// await this.autoUpdater_.checkForUpdatesAndNotify();
// } catch (error) {
// this.autoUpdateLogger_.error(error);
// }
// }
checkForUpdates(inBackground, window, logFilePath) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, window, logFilePath);
}
}

View File

@@ -0,0 +1,144 @@
const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
let autoUpdateLogger_ = new Logger();
let checkInBackground_ = false;
let isCheckingForUpdate_ = false;
let parentWindow_ = null;
// Note: Electron Builder's autoUpdater is incredibly buggy so currently it's only used
// to detect if a new version is present. If it is, the download link is simply opened
// in a new browser window.
autoUpdater.autoDownload = false;
function htmlToText_(html) {
let output = html.replace(/\n/g, '');
output = output.replace(/<li>/g, '- ');
output = output.replace(/<p>/g, '');
output = output.replace(/<\/p>/g, '\n');
output = output.replace(/<\/li>/g, '\n');
output = output.replace(/<ul>/g, '');
output = output.replace(/<\/ul>/g, '');
output = output.replace(/<.*?>/g, '');
output = output.replace(/<\/.*?>/g, '');
return output;
}
function showErrorMessageBox(message) {
return dialog.showMessageBox(parentWindow_, {
type: 'error',
message: message,
});
}
function onCheckStarted() {
autoUpdateLogger_.info('checkForUpdates: Starting...');
isCheckingForUpdate_ = true;
}
function onCheckEnded() {
autoUpdateLogger_.info('checkForUpdates: Done.');
isCheckingForUpdate_ = false;
}
autoUpdater.on('error', (error) => {
autoUpdateLogger_.error(error);
if (checkInBackground_) return onCheckEnded();
showErrorMessageBox(error == null ? "unknown" : (error.stack || error).toString())
onCheckEnded();
})
function findDownloadFilename_(info) {
// { version: '1.0.64',
// files:
// [ { url: 'Joplin-1.0.64-mac.zip',
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==' },
// { url: 'Joplin-1.0.64.dmg',
// sha512: 'jAewQQoJ3nCaOj8hWDgf0sc3LBbAWQtiKqfTflK8Hc3Dh7fAy9jRHfFAZKFUZ9ll95Bun0DVsLq8wLSUrdsMXw==',
// size: 77104485 } ],
// path: 'Joplin-1.0.64-mac.zip',
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==',
// releaseDate: '2018-02-16T00:13:01.634Z',
// releaseName: 'v1.0.64',
// releaseNotes: '<p>Still more fixes and im...' }
if (!info) return null;
if (!info.files) {
// info.path seems to contain a default, though not a good one,
// so the loop below if preferable to find the right file.
return info.path;
}
for (let i = 0; i < info.files.length; i++) {
const f = info.files[i].url; // Annoyingly this is called "url" but it's obviously not a url, so hopefully it won't change later on and become one.
if (f.indexOf('.exe') >= 0) return f;
if (f.indexOf('.dmg') >= 0) return f;
}
return info.path;
}
autoUpdater.on('update-available', (info) => {
const filename = findDownloadFilename_(info);
if (!info.version || !filename) {
if (checkInBackground_) return onCheckEnded();
showErrorMessageBox(('Could not get version info: ' + JSON.stringify(info)));
return onCheckEnded();
}
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/v' + info.version + '/' + filename;
let releaseNotes = info.releaseNotes + '';
if (releaseNotes) releaseNotes = '\n\n' + _('Release notes:\n\n%s', htmlToText_(releaseNotes));
const buttonIndex = dialog.showMessageBox(parentWindow_, {
type: 'info',
message: _('An update is available, do you want to download it now?' + releaseNotes),
buttons: [_('Yes'), _('No')]
});
onCheckEnded();
if (buttonIndex === 0) require('electron').shell.openExternal(downloadUrl);
})
autoUpdater.on('update-not-available', () => {
if (checkInBackground_) return onCheckEnded();
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
onCheckEnded();
})
function checkForUpdates(inBackground, window, logFilePath) {
if (isCheckingForUpdate_) {
autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running');
return;
}
parentWindow_ = window;
onCheckStarted();
if (logFilePath && !autoUpdateLogger_.targets().length) {
autoUpdateLogger_ = new Logger();
autoUpdateLogger_.addTarget('file', { path: logFilePath });
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
autoUpdateLogger_.info('checkForUpdates: Initializing...');
autoUpdater.logger = autoUpdateLogger_;
}
checkInBackground_ = inBackground;
try {
autoUpdater.checkForUpdates()
} catch (error) {
autoUpdateLogger_.error(error);
if (!checkInBackground_) showErrorMessageBox(error.message);
onCheckEnded();
}
}
module.exports.checkForUpdates = checkForUpdates

View File

@@ -0,0 +1,3 @@
owner: laurent22
repo: joplin
provider: github

View File

@@ -7,14 +7,22 @@ const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const pathUtils = require('lib/path-utils.js');
const { _ } = require('lib/locale.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const shared = require('lib/components/shared/config-shared.js');
class ConfigScreenComponent extends React.Component {
constructor() {
super();
this.state = {
settings: {},
shared.init(this);
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
}
this.rowStyle_ = {
marginBottom: 10,
};
}
@@ -44,9 +52,7 @@ class ConfigScreenComponent extends React.Component {
let output = null;
const rowStyle = {
marginBottom: 10,
};
const rowStyle = this.rowStyle_;
const labelStyle = Object.assign({}, theme.textStyle, {
display: 'inline-block',
@@ -58,9 +64,7 @@ class ConfigScreenComponent extends React.Component {
};
const updateSettingValue = (key, value) => {
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
this.setState({ settings: settings });
return shared.updateSettingValue(this, key, value);
}
// Component key needs to be key+value otherwise it doesn't update when the settings change.
@@ -89,24 +93,39 @@ 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 inputStyle = Object.assign({}, controlStyle, { width: '50%', minWidth: '20em' });
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={inputStyle} 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 {
@@ -117,10 +136,7 @@ class ConfigScreenComponent extends React.Component {
}
onSaveClick() {
for (let n in this.state.settings) {
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
shared.saveSettings(this);
this.props.dispatch({ type: 'NAV_BACK' });
}
@@ -130,7 +146,7 @@ class ConfigScreenComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const style = Object.assign({}, this.props.style, { overflow: 'auto' });
const settings = this.state.settings;
const headerStyle = {
@@ -142,23 +158,28 @@ class ConfigScreenComponent extends React.Component {
};
const buttonStyle = {
display: this.state.settings === this.props.settings ? 'none' : 'inline-block',
display: this.state.changedSettingKeys.length ? 'inline-block' : 'none',
marginRight: 10,
}
let settingComps = [];
let keys = Setting.keys(true, 'desktop');
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!(key in settings)) {
console.warn('Missing setting: ' + key);
continue;
}
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
const settingComps = shared.settingsToComponents(this, 'desktop', settings);
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
{messages[0]}
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
</div>);
settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
{ statusComp }
</div>);
}
return (

View File

@@ -92,10 +92,14 @@ class EncryptionConfigScreenComponent extends React.Component {
};
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@@ -121,7 +125,7 @@ class EncryptionConfigScreenComponent extends React.Component {
}
}
const decryptedItemsInfo = this.props.encryptionEnabled ? <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p> : null;
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;
@@ -149,11 +153,36 @@ class EncryptionConfigScreenComponent extends React.Component {
);
}
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<tr key={id}><td style={theme.textStyle}>{id}</td></tr>);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{ rows }
</tbody>
</table>
</div>
);
}
return (
<div>
<Header style={headerStyle} />
<div style={containerStyle}>
<div style={{backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
{/*<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>
@@ -163,12 +192,13 @@ class EncryptionConfigScreenComponent extends React.Component {
<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>
</div>*/}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
{decryptedItemsInfo}
{toggleButton}
{masterKeySection}
{nonExistingMasterKeySection}
</div>
</div>
);
@@ -183,6 +213,7 @@ const mapStateToProps = (state) => {
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
};

View File

@@ -25,9 +25,7 @@ class ImportScreenComponent extends React.Component {
doImport: true,
filePath: newProps.filePath,
messages: [],
});
this.doImport();
}, () => { this.doImport() });
}
}

View File

@@ -163,6 +163,8 @@ class MainScreenComponent extends React.Component {
}
},
});
} else if (command.name === 'toggleVisiblePanes') {
this.toggleVisiblePanes();
} else if (command.name === 'editAlarm') {
const note = await Note.load(command.noteId);
@@ -274,20 +276,22 @@ class MainScreenComponent extends React.Component {
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);
const selectedFolderId = this.props.selectedFolderId;
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
const headerButtons = [];
headerButtons.push({
title: _('New note'),
iconName: 'fa-file-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newNote' }) },
});
headerButtons.push({
title: _('New to-do'),
iconName: 'fa-check-square-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newTodo' }) },
});
@@ -307,9 +311,7 @@ class MainScreenComponent extends React.Component {
title: _('Layout'),
iconName: 'fa-columns',
enabled: !!notes.length,
onClick: () => {
this.toggleVisiblePanes();
},
onClick: () => { this.doCommand({ name: 'toggleVisiblePanes' }) },
});
if (!this.promptOnClose_) {
@@ -384,6 +386,7 @@ const mapStateToProps = (state) => {
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
selectedFolderId: state.selectedFolderId,
};
};

View File

@@ -16,6 +16,7 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { shim } = require('lib/shim.js');
const eventManager = require('../eventManager');
const fs = require('fs-extra');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@@ -115,10 +116,14 @@ class NoteTextComponent extends React.Component {
eventManager.removeListener('todoToggle', this.onTodoToggle_);
}
async saveIfNeeded() {
async saveIfNeeded(saveIfNewNote = false) {
const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id);
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
this.scheduleSaveTimeout_ = null;
if (!shared.isModified(this)) return;
if (!forceSave) {
if (!shared.isModified(this)) return;
}
await shared.saveNoteButton_press(this);
}
@@ -190,13 +195,21 @@ class NoteTextComponent extends React.Component {
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
if (note) {
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
if (Setting.value(focusSettingName) === 'title') {
if (this.titleField_) this.titleField_.focus();
} else {
if (this.editor_) this.editor_.editor.focus();
}
}
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);
}
@@ -252,7 +265,7 @@ class NoteTextComponent extends React.Component {
shared.showMetadata_onPress(this);
}
webview_ipcMessage(event) {
async webview_ipcMessage(event) {
const msg = event.channel ? event.channel : '';
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
@@ -274,6 +287,32 @@ class NoteTextComponent extends React.Component {
} else if (msg === 'percentScroll') {
this.ignoreNextEditorScroll_ = true;
this.setEditorPercentScroll(arg0);
} else if (msg === 'contextMenu') {
const itemType = arg0 && arg0.type;
const menu = new Menu()
if (itemType === 'image') {
const resource = await Resource.load(arg0.resourceId);
const resourcePath = Resource.fullPath(resource);
menu.append(new MenuItem({label: _('Open...'), click: async () => {
bridge().openExternal(resourcePath);
}}));
menu.append(new MenuItem({label: _('Save as...'), click: async () => {
const filePath = bridge().showSaveDialog({
defaultPath: resource.filename ? resource.filename : resource.title,
});
if (!filePath) return;
await fs.copy(resourcePath, filePath);
}}));
} else {
reg.logger().error('Unhandled item type: ' + itemType);
return;
}
menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) {
const resourceId = msg.substr('joplin://'.length);
Resource.load(resourceId).then((resource) => {
@@ -392,16 +431,13 @@ class NoteTextComponent extends React.Component {
async commandAttachFile() {
const noteId = this.props.noteId;
if (!noteId) return;
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory', 'multiSelections'],
});
if (!filePaths || !filePaths.length) return;
await this.saveIfNeeded();
let note = await Note.load(noteId);
await this.saveIfNeeded(true);
let note = await Note.load(this.state.note.id);
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
@@ -419,20 +455,29 @@ class NoteTextComponent extends React.Component {
}
}
commandSetAlarm() {
const noteId = this.props.noteId;
if (!noteId) return;
async commandSetAlarm() {
await this.saveIfNeeded(true);
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'editAlarm',
noteId: noteId,
noteId: this.state.note.id,
});
}
async commandSetTags() {
await this.saveIfNeeded(true);
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'setTags',
noteId: this.state.note.id,
});
}
itemContextMenu(event) {
const noteId = this.props.noteId;
if (!noteId) return;
const note = this.state.note;
if (!note) return;
const menu = new Menu()
@@ -440,10 +485,16 @@ class NoteTextComponent extends React.Component {
return this.commandAttachFile();
}}));
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
return this.commandSetAlarm();
menu.append(new MenuItem({label: _('Tags'), click: async () => {
return this.commandSetTags();
}}));
if (!!note.is_todo) {
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
return this.commandSetAlarm();
}}));
}
menu.popup(bridge().window());
}
@@ -453,6 +504,7 @@ class NoteTextComponent extends React.Component {
const body = note && note.body ? note.body : '';
const theme = themeStyle(this.props.theme);
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
const isTodo = note && !!note.is_todo;
const borderWidth = 1;
@@ -560,6 +612,12 @@ class NoteTextComponent extends React.Component {
onClick: () => { return this.commandAttachFile(); },
});
toolbarItems.push({
title: _('Tags'),
iconName: 'fa-tags',
onClick: () => { return this.commandSetTags(); },
});
if (note.is_todo) {
toolbarItems.push({
title: Note.needAlarm(note) ? time.formatMsToLocal(note.todo_due) : _('Set alarm'),
@@ -576,9 +634,11 @@ class NoteTextComponent extends React.Component {
const titleEditor = <input
type="text"
ref={(elem) => { this.titleField_ = elem; } }
style={titleEditorStyle}
value={note && note.title ? note.title : ''}
onChange={(event) => { this.title_changeText(event); }}
placeholder={ this.props.newNote ? _('Creating new %s...', isTodo ? _('to-do') : _('note')) : '' }
/>
const titleBarMenuButton = <IconButton style={{

View File

@@ -1,179 +1,215 @@
<style>
body {
overflow: hidden;
}
#content {
overflow-y: auto;
height: 100%;
padding-left: 10px;
padding-right: 10px;
}
</style>
<div id="hlScriptContainer"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script>
const { ipcRenderer } = require('electron');
const contentElement = document.getElementById('content');
// ----------------------------------------------------------------------
// Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
let hljsScriptAdded = false;
let hljsLoaded = false;
function loadHljs(callback) {
hljsScriptAdded = true;
const script = document.createElement('script');
script.onload = function () {
hljsLoaded = true;
applyHljs();
};
script.src = 'highlight/highlight.pack.js';
document.getElementById('hlScriptContainer').appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
link.href = 'highlight/styles/atom-one-light.css';
document.getElementById('hlScriptContainer').appendChild(link);
}
function loadAndApplyHljs() {
var codeElements = document.getElementsByClassName('code');
if (!codeElements.length) return;
if (!hljsScriptAdded) {
this.loadHljs();
return;
}
// If HLJS is not loaded yet, no need to do anything. When it loads
// it will automatically apply the style to all the code elements.
if (hljsLoaded) applyHljs(codeElements);
}
function applyHljs(codeElements) {
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
for (var i = 0; i < codeElements.length; i++) {
hljs.highlightBlock(codeElements[i]);
}
}
// ----------------------------------------------------------------------
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
// one second after the content has been updated.
//
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
let percentScroll_ = 0;
let checkScrollIID_ = null;
function setPercentScroll(percent) {
percentScroll_ = percent;
contentElement.scrollTop = percentScroll_ * maxScrollTop();
}
function percentScroll() {
return percentScroll_;
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
ipcRenderer.on('setHtml', (event, html) => {
contentElement.innerHTML = html;
loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i];
const ul = cb.parentElement.parentElement;
if (!ul) {
console.warn('Unexpected layout for checkbox');
continue;
<!DOCTYPE html>
<html>
<head>
<style>
body {
overflow: hidden;
}
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
#content {
overflow-y: auto;
height: 100%;
padding-left: 10px;
padding-right: 10px;
}
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
</style>
</head>
<body id="body">
<div id="hlScriptContainer"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script>
const { ipcRenderer } = require('electron');
const contentElement = document.getElementById('content');
// ----------------------------------------------------------------------
// Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
let hljsScriptAdded = false;
let hljsLoaded = false;
function loadHljs(callback) {
hljsScriptAdded = true;
const script = document.createElement('script');
script.onload = function () {
hljsLoaded = true;
applyHljs();
};
script.src = 'highlight/highlight.pack.js';
document.getElementById('hlScriptContainer').appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
link.href = 'highlight/styles/atom-one-light.css';
document.getElementById('hlScriptContainer').appendChild(link);
}
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
restorePercentScroll();
function loadAndApplyHljs() {
var codeElements = document.getElementsByClassName('code');
if (!codeElements.length) return;
if (!checkScrollIID_) {
checkScrollIID_ = setInterval(() => {
const h = contentElement.scrollHeight;
if (h !== previousContentHeight) {
previousContentHeight = h;
ignoreNextScrollEvent = true;
restorePercentScroll();
if (!hljsScriptAdded) {
this.loadHljs();
return;
}
// If HLJS is not loaded yet, no need to do anything. When it loads
// it will automatically apply the style to all the code elements.
if (hljsLoaded) applyHljs(codeElements);
}
function applyHljs(codeElements) {
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
for (var i = 0; i < codeElements.length; i++) {
try {
hljs.highlightBlock(codeElements[i]);
} catch (error) {
console.warn('Cannot highlight code', error);
}
if (Date.now() - startTime >= 1000) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
}
// ----------------------------------------------------------------------
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
// one second after the content has been updated.
//
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
let percentScroll_ = 0;
let checkScrollIID_ = null;
function setPercentScroll(percent) {
percentScroll_ = percent;
contentElement.scrollTop = percentScroll_ * maxScrollTop();
}
function percentScroll() {
return percentScroll_;
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
ipcRenderer.on('setHtml', (event, html) => {
updateBodyHeight();
contentElement.innerHTML = html;
loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i];
const ul = cb.parentElement.parentElement;
if (!ul) {
console.warn('Unexpected layout for checkbox');
continue;
}
}, 1);
}
});
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
}
let ignoreNextScrollEvent = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
if (checkScrollIID_) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
restorePercentScroll();
if (!checkScrollIID_) {
checkScrollIID_ = setInterval(() => {
const h = contentElement.scrollHeight;
if (h !== previousContentHeight) {
previousContentHeight = h;
ignoreNextScrollEvent = true;
restorePercentScroll();
}
if (Date.now() - startTime >= 1000) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
}, 1);
}
});
let ignoreNextScrollEvent = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
if (checkScrollIID_) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
ignoreNextScrollEvent = true;
setPercentScroll(percent);
});
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
ignoreNextScrollEvent = true;
setPercentScroll(percent);
});
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScrollEvent) {
ignoreNextScrollEvent = false;
return;
// The body element needs to have a fixed height for the content to be scrollable
function updateBodyHeight() {
document.getElementById('body').style.height = window.innerHeight + 'px';
}
const m = maxScrollTop();
const percent = m ? contentElement.scrollTop / m : 0;
setPercentScroll(percent);
ipcRenderer.sendToHost('percentScroll', percent);
});
// Disable drag and drop otherwise it's possible to drop a URL
// on it and it will open in the view as a website.
document.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
</script>
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScrollEvent) {
ignoreNextScrollEvent = false;
return;
}
const m = maxScrollTop();
const percent = m ? contentElement.scrollTop / m : 0;
setPercentScroll(percent);
ipcRenderer.sendToHost('percentScroll', percent);
});
document.addEventListener('contextmenu', function(event) {
const element = event.target;
if (element && element.getAttribute('data-resource-id')) {
ipcRenderer.sendToHost('contextMenu', {
type: element.getAttribute('src') ? 'image' : 'link',
resourceId: element.getAttribute('data-resource-id'),
});
}
});
// Disable drag and drop otherwise it's possible to drop a URL
// on it and it will open in the view as a website.
document.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
window.addEventListener('resize', function() {
updateBodyHeight();
});
updateBodyHeight();
</script>
</body>
</html>

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

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@ locales['en_GB'] = require('./en_GB.json');
locales['de_DE'] = require('./de_DE.json');
locales['es_CR'] = require('./es_CR.json');
locales['es_ES'] = require('./es_ES.json');
locales['eu'] = require('./eu.json');
locales['fr_FR'] = require('./fr_FR.json');
locales['hr_HR'] = require('./hr_HR.json');
locales['it_IT'] = require('./it_IT.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

View File

@@ -3,6 +3,15 @@
// Make it possible to require("/lib/...") without specifying full path
require('app-module-path').addPath(__dirname);
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
};
const { app } = require('./app.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "1.0.64",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -29,9 +29,9 @@
"optional": true
},
"@types/node": {
"version": "7.0.46",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz",
"integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==",
"version": "7.0.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.52.tgz",
"integrity": "sha512-jjpyQsKGsOF/wUElNjfPULk+d8PKvJOIXk3IUeBYYmNCy5dMWfrI+JiixYNw8ppKOlcRwWTXFl0B+i5oGrf95Q==",
"dev": true
},
"ajv": {
@@ -237,6 +237,11 @@
"integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
"dev": true
},
"async-mutex": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -563,8 +568,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"base64-js": {
"version": "1.2.0",
@@ -604,6 +613,14 @@
"readable-stream": "2.3.3"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
"requires": {
"inherits": "2.0.3"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -721,7 +738,6 @@
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
@@ -866,8 +882,7 @@
"capture-stack-trace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz",
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
"dev": true
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0="
},
"caseless": {
"version": "0.12.0",
@@ -924,7 +939,8 @@
"chownr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE="
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
"dev": true
},
"chromium-pickle-js": {
"version": "0.2.0",
@@ -968,9 +984,9 @@
"dev": true
},
"color": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz",
"integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz",
"integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=",
"requires": {
"color-convert": "1.9.1",
"color-string": "1.5.2"
@@ -1021,8 +1037,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.0",
@@ -1084,7 +1099,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
"dev": true,
"requires": {
"capture-stack-trace": "1.0.0"
}
@@ -1183,14 +1197,6 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "1.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -1227,11 +1233,6 @@
"repeating": "2.0.1"
}
},
"detect-libc": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
"integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U="
},
"dmg-builder": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-2.1.6.tgz",
@@ -1282,8 +1283,7 @@
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"ecc-jsbn": {
"version": "0.1.1",
@@ -1302,12 +1302,12 @@
"dev": true
},
"electron": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
"integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.11.tgz",
"integrity": "sha1-mTtqp54OeafPzDafTIE/vZoLCNk=",
"dev": true,
"requires": {
"@types/node": "7.0.46",
"@types/node": "7.0.52",
"electron-download": "3.3.0",
"extract-zip": "1.6.6"
}
@@ -2011,8 +2011,18 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
"requires": {
"graceful-fs": "4.1.11",
"inherits": "2.0.3",
"mkdirp": "0.5.1",
"rimraf": "2.6.2"
}
},
"fullstore": {
"version": "1.1.0",
@@ -2058,8 +2068,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"getpass": {
"version": "0.1.7",
@@ -2080,7 +2089,6 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
@@ -2129,7 +2137,6 @@
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
"create-error-class": "3.0.2",
"duplexer3": "0.1.4",
@@ -2147,8 +2154,7 @@
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=",
"dev": true
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
}
}
},
@@ -2308,7 +2314,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
@@ -2507,14 +2512,12 @@
"is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
"dev": true
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
},
"is-retry-allowed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=",
"dev": true
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
},
"is-stream": {
"version": "1.1.0",
@@ -2667,6 +2670,14 @@
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"katex": {
"version": "0.9.0-beta1",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
"requires": {
"match-at": "0.1.1"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -2799,8 +2810,7 @@
"lowercase-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz",
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=",
"dev": true
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY="
},
"lru-cache": {
"version": "4.1.1",
@@ -2847,6 +2857,29 @@
"uc.micro": "1.0.3"
}
},
"markdown-it-katex": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz",
"integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=",
"requires": {
"katex": "0.6.0"
},
"dependencies": {
"katex": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz",
"integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=",
"requires": {
"match-at": "0.1.1"
}
}
}
},
"match-at": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -2950,16 +2983,10 @@
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
"dev": true
},
"mimic-response": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",
"integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
}
@@ -2969,22 +2996,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz",
"integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==",
"requires": {
"yallist": "3.0.2"
}
},
"minizlib": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.4.tgz",
"integrity": "sha512-sN4U9tIJtBRwKbwgFh9qJfrPIQ/GGTRr1MGqkgOeMTLy8/lM0FcWU//FqlnZ3Vb7gJ+Mxh3FOg1EklibdajbaQ==",
"requires": {
"minipass": "2.2.1"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@@ -3287,8 +3298,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
@@ -3414,8 +3424,7 @@
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
},
"preserve": {
"version": "0.2.0",
@@ -3526,6 +3535,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="
},
"rabin-bindings": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/rabin-bindings/-/rabin-bindings-1.7.3.tgz",
@@ -3862,11 +3876,15 @@
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"requires": {
"glob": "7.1.2"
}
@@ -3923,17 +3941,16 @@
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"sharp": {
"version": "0.18.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.18.4.tgz",
"integrity": "sha1-/jKcDwaJbCiqJDdt8f/wKuV/LTQ=",
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.17.3.tgz",
"integrity": "sha1-SEzSpwyQA3CUjcxD4WX3gwa/9Io=",
"requires": {
"caw": "2.0.1",
"color": "2.0.1",
"detect-libc": "0.2.0",
"color": "1.0.3",
"got": "6.7.1",
"nan": "2.7.0",
"semver": "5.4.1",
"simple-get": "2.7.0",
"tar": "3.2.1"
"tar": "2.2.1"
}
},
"shebang-command": {
@@ -3962,21 +3979,6 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"simple-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
},
"simple-get": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz",
"integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==",
"requires": {
"decompress-response": "3.3.0",
"once": "1.4.0",
"simple-concat": "1.0.0"
}
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -4921,15 +4923,13 @@
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
},
"tar": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz",
"integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
"requires": {
"chownr": "1.0.1",
"minipass": "2.2.1",
"minizlib": "1.0.4",
"mkdirp": "0.5.1",
"yallist": "3.0.2"
"block-stream": "0.0.9",
"fstream": "1.0.11",
"inherits": "2.0.3"
}
},
"tar-fs": {
@@ -5047,8 +5047,7 @@
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
},
"to-fast-properties": {
"version": "1.0.3",
@@ -5194,11 +5193,19 @@
}
}
},
"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-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
"dev": true,
"requires": {
"prepend-http": "1.0.4"
}
@@ -5334,6 +5341,22 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"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"
},
"dependencies": {
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
}
}
},
"xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
@@ -5361,11 +5384,6 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
},
"yargs": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "1.0.64",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -8,8 +8,8 @@
"pack": "node_modules/.bin/electron-builder --dir",
"dist": "node_modules/.bin/electron-builder",
"publish": "build -p always",
"postinstall": "node compile-jsx.js && node compile-package-info.js",
"compile": "node compile-jsx.js && node compile-package-info.js"
"postinstall": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
"compile": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
},
"repository": {
"type": "git",
@@ -22,6 +22,9 @@
},
"build": {
"appId": "net.cozic.joplin-desktop",
"extraResources": [
"build/icons/*"
],
"win": {
"icon": "../../Assets/Joplin.ico"
},
@@ -34,14 +37,15 @@
"asar": false
},
"linux": {
"asar": false
"asar": false,
"category": "Office"
}
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^1.7.9",
"electron": "^1.7.11",
"electron-builder": "^19.45.4"
},
"optionalDependencies": {
@@ -51,6 +55,8 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"electron-context-menu": "^0.9.1",
"electron-log": "^2.2.11",
"electron-updater": "^2.16.1",
@@ -61,9 +67,11 @@
"highlight.js": "^9.12.0",
"html-entities": "^1.2.1",
"jssha": "^2.3.1",
"katex": "^0.9.0-beta1",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
"markdown-it-katex": "^2.0.3",
"md5": "^2.2.1",
"mime": "^2.0.3",
"moment": "^2.19.1",
@@ -77,13 +85,15 @@
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"sharp": "^0.18.4",
"sharp": "^0.17.3",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tcp-port-used": "^0.1.2",
"uuid": "^3.1.0"
"url-parse": "^1.2.0",
"uuid": "^3.1.0",
"xml2js": "^0.4.19"
}
}

View File

@@ -1,7 +1,7 @@
const Setting = require('lib/models/Setting.js');
const globalStyle = {
fontSize: 12,
fontSize: 12 * Setting.value('style.zoom')/100,
fontFamily: 'sans-serif',
margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10,

View File

@@ -79,6 +79,10 @@ async function main(argv) {
const macOsUrl = downloadUrl(release, 'macos');
const linuxUrl = downloadUrl(release, 'linux');
console.info('Windows: ', winUrl);
console.info('macOS: ', macOsUrl);
console.info('Linux: ', linuxUrl);
let content = readmeContent();
if (winUrl) content = content.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.exe)/, winUrl);

20
LICENSE
View File

@@ -1,7 +1,21 @@
MIT License
Copyright (c) 2016-2018 Laurent Cozic
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1,5 @@
License MIT
Copyright (c) 2016-2018 Laurent Cozic
L'autorisation est accordée, gracieusement, à toute personne acquérant une copie de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de commercialiser le Logiciel sans restriction, notamment les droits d'utiliser, de copier, de modifier, de fusionner, de publier, de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel, ainsi que d'autoriser les personnes auxquelles le Logiciel est fourni à le faire, sous réserve des conditions suivantes :

130
README.md
View File

@@ -4,9 +4,9 @@ Joplin is a free, open source note taking and to-do application, which can handl
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
The notes can be [synchronised](#synchronisation) with various targets including [Nextcloud](https://nextcloud.com/), the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
Joplin is still under development but is out of Beta and should be suitable for every day use. The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
@@ -18,15 +18,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download
-----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-Setup-0.10.43.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.64/Joplin-Setup-1.0.64.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.64/Joplin-1.0.64.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.64/Joplin-1.0.64-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## Mobile applications
Operating System | Download | Alt. Download
-----------------|---------------------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.75/joplin-v0.10.75.apk)
-----------------|----------|----------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.98/joplin-v1.0.98.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -51,13 +51,15 @@ For usage information, please refer to the full [Joplin Terminal Application Doc
# Features
- Desktop, mobile and terminal applications.
- Import Enex files (Evernote export format)
- Support notes, to-dos, tags and notebooks.
- Support for alarms (notifications) in mobile and desktop applications.
- Offline first, so the entire data is always available on the device even without an internet connection.
- Ability to synchronise with multiple targets, including the file system and OneDrive (NextCloud and Dropbox are planned).
- Synchronisation with various services, including NextCloud, WebDAV and OneDrive. Dropbox is planned.
- End To End Encryption (E2EE)
- Synchronises to a plain text format, which can be easily manipulated, backed up, or exported to a different format.
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications.
- Tag support
- File attachment support (images are displayed, and other files are linked and can be opened in the relevant application).
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications. Support for extra features such as math notation and checkboxes.
- File attachment support - images are displayed, and other files are linked and can be opened in the relevant application.
- Search functionality.
- Geo-location support.
- Supports multiple languages
@@ -85,13 +87,41 @@ In general the way to import notes from any application into Joplin is to conver
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
On the **desktop application**, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
## Nextcloud synchronisation
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
On the **desktop application** or **mobile application**, go to the config screen and select Nextcloud as the synchronisation target. Then input [the WebDAV URL](https://docs.nextcloud.com/server/9/user_manual/files/access_webdav.html), this is normally `https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin` (make sure to create the "Joplin" directory in Nextcloud and to replace USERNAME by your Nextcloud username), and set the username and password.
On the **terminal application**, you will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
:config sync.target 5
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
Known compatible services that use WebDAV:
- [Box.com](https://www.box.com/)
- [DriveHQ](https://www.drivehq.com)
- [Zimbra](https://www.zimbra.com/)
- [Seafile](https://www.seafile.com/)
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
On the **desktop application** or **mobile application**, select "OneDrive" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
@@ -119,9 +149,47 @@ On mobile, the alarms will be displayed using the built-in notification system.
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
# Markdown
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
## Math notation
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:
$$
EXPRESSION
$$
For example:
$$
f(x) = \int_{-\infty}^\infty
\hat f(\xi)\,e^{2 \pi i \xi x}
\,d\xi
$$
Here is an example with the Markdown and rendered result side by side:
<img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/Katex.png" style="max-width: 100%; max-height: 35em;">
## Checkboxes
Checkboxes can be added like so:
-[ ] Milk
-[ ] Rice
-[ ] Eggs
The checkboxes can then be ticked in the mobile and desktop applications.
# Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
# Localisation
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
@@ -130,23 +198,31 @@ Joplin is currently available in English, French, Spanish, German, Portuguese, C
This translation will apply to the three applications - desktop, mobile and terminal.
# Contributing
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from the table below.
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
Current translations:
# Coming features
- NextCloud support
- All: End to end encryption
- Windows: Tray icon
- Desktop apps: Tag auto-complete
- Desktop apps: Dark theme
- Linux: Enable auto-update for desktop app
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 87%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić <trbuhom@net.hr> | 71%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Tobias Strobel <git@strobeltobias.de> | 89%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín <f@mrtn.es> | 97%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cr.png) | Español (Costa Rica) | [es_CR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_CR.po) | | 65%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 99%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 73%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 87%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | | 71%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 91%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | RCJacH <RCJacH@outlook.com> | 73%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 71%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs
- Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.
- Auto-update is not working in the Linux desktop application.
- While the mobile can sync and load tags, it is not currently possible to create new ones. The desktop and terminal apps can create, delete and edit tags.
# License

View File

@@ -0,0 +1,4 @@
# When I open a note in vim, the cursor is not visible
It seems to be due to the setting `set term=ansi` in .vimrc. Removing it should fix the issue. See https://github.com/laurent22/joplin/issues/147 for more information.

View File

@@ -35,7 +35,7 @@ To start it, type `demo-joplin`.
# Usage
To start the application type `joplin`. This will open the user interface, which has three main panes: Notebooks, Notes and the text of the current note. There are also additional panels that can be toggled on and off via [shortcuts](#available-shortcuts).
To start the application type `joplin`. This will open the user interface, which has three main panes: Notebooks, Notes and the text of the current note. There are also additional panels that can be toggled on and off via [shortcuts](#shortcuts).
<img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/ScreenshotTerminalCaptions.png" height="450px">
@@ -45,11 +45,11 @@ Joplin user interface is partly based on the text editor Vim and offers two diff
### Normal mode
Allows moving from one pane to another using the `Tab` and `Shift-Tab` keys, and to select/view notes using the arrow keys. Text area can be scrolled using the arrow keys too. Press `Enter` to edit a note. Various other [shortcuts](#available-shortcuts) are available.
Allows moving from one pane to another using the `Tab` and `Shift-Tab` keys, and to select/view notes using the arrow keys. Text area can be scrolled using the arrow keys too. Press `Enter` to edit a note. Various other [shortcuts](#shortcuts) are available.
### Command-line mode
Press `:` to enter command line mode. From there, the Joplin commands such as `mknote` or `search` are available. See the [full list of commands](#available-commands).
Press `:` to enter command line mode. From there, the Joplin commands such as `mknote` or `search` are available. See the [full list of commands](#commands).
It is possible to refer to a note or notebook by title or ID. However the simplest way is to refer to the currently selected item using one of these shortcuts:
@@ -96,7 +96,7 @@ The complete usage information is available from command-line mode, by typing on
Command | Description
--------|-------------------
`help` | General help information
`help shortcuts` | Lists the available shortcuts
`help keymap` | Lists the available shortcuts
`help [command]` | Displays information about a particular command
If the help is not fully visible, press `Tab` multiple times till the console is in focus and use the arrow keys or page up/down to scroll the text.
@@ -111,11 +111,30 @@ To import Evernote data, follow these steps:
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A Dropbox driver will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
## Nextcloud synchronisation
You will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
:config sync.target 5
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
If synchronisation does not work, please consult the logs in the app profile directory (`~/.config/joplin`)- it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
@@ -156,29 +175,82 @@ Give a new title to the note:
$ joplin set fe889 title "New title"
# Available shortcuts
# Shortcuts
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of shortcuts:
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of default shortcuts:
Tab Give focus to next pane
Shift+Tab Give focus to previous pane
: Enter command line mode
ESC Exit command line mode
ENTER Edit the selected note
Ctrl+C Cancel the current command.
Ctrl+D Exit the application.
DELETE Delete the currently selected note or notebook.
SPACE Set a to-do as completed / not completed
tc [t]oggle [c]onsole between maximized/minimized/hidden/visible.
/ Search
tm [t]oggle note [m]etadata.
mn [M]ake a new [n]ote
mt [M]ake a new [t]odo
mb [M]ake a new note[b]ook
yn Copy ([Y]ank) the [n]ote to a notebook.
dn Move the note to a notebook.
: enter_command_line_mode
TAB focus_next
SHIFT_TAB focus_previous
UP move_up
DOWN move_down
PAGE_UP page_up
PAGE_DOWN page_down
ENTER activate
DELETE, BACKSPACE delete
(SPACE) todo toggle $n
tc toggle_console
tm toggle_metadata
/ search ""
mn mknote ""
mt mktodo ""
mb mkbook ""
yn cp $n ""
dn mv $n ""
# Available commands
Shortcut can be configured by adding a keymap file to the profile directory in `~/.config/joplin/keymap.json`. The content of this file is a JSON array with each entry defining a command and the keys associated with it.
As an example, this is the default keymap, but read below for a detailed explanation of each property.
```json
[
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
]
```
Each entry can have the following properties:
Name | Description
-----|------------
`keys` | The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the [list of available special keys](https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531). For example, `['DELETE', 'BACKSPACE']` means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example "tc" means that the command will be executed when the user pressed "t" then "c". Special keys can also be used in this fashion - simply write them one after the other. For instance, `CTRL_WCTRL_W` means the action would be executed if the user pressed "ctrl-w ctrl-w".
`type` | The command type. It can have the value "exec", "function" or "prompt". **exec**: Simply execute the provided [command](#commands). For example `edit $n` would edit the selected note. **function**: Run a special commands (see below for the list of functions). **prompt**: A bit similar to "exec", except that the command is not going to be executed immediately - this allows the user to provide additional data. For example `mknote ""` would fill the command line with this command and allow the user to set the title. A prompt command can also take a `cursorPosition` parameter (see below)
`command` | The command that needs to be executed
`cusorPosition` | An integer. For prompt commands, tells where the cursor (caret) should start at. This is convenient for example to position the cursor between quotes. Use a negative value to set a position starting from the end. A value of "0" means positioning the caret at the first character. A value of "-1" means positioning it at the end.
This is the list of special functions:
Name | Description
-----|------------
enter_command_line_mode | Enter command line mode
focus_next | Focus next pane (or widget)
focus_previous | Focus previous pane (or widget)
move_up | Move up (in a list for example)
move_down | Move down (in a list for example)
page_up | Page up
page_down | Page down
activate | Activates the selected item. If the item is a note for example it will be open in the editor
delete | Deletes the selected item
toggle_console | Toggle the console
toggle_metadata | Toggle note metadata
# Commands
The following commands are available in [command-line mode](#command-line-mode):
@@ -186,6 +258,12 @@ The following commands are available in [command-line mode](#command-line-mode):
Attaches the given file to the note.
cat <note>
Displays the given note.
-v, --verbose Displays the complete information about note.
config [name] [value]
Gets or sets a config value. If [value] is not provided, it will show the
@@ -196,11 +274,6 @@ The following commands are available in [command-line mode](#command-line-mode):
Possible keys/values:
sync.2.path File system synchronisation target directory.
The path to synchronise with when file system
synchronisation is enabled. See `sync.target`.
Type: string.
editor Text editor.
The editor that will be used to open a note. If
none is provided it will try to auto-detect the
@@ -209,8 +282,12 @@ The following commands are available in [command-line mode](#command-line-mode):
locale Language.
Type: Enum.
Possible values: en_GB (English), es_CR (Español),
fr_FR (Français).
Possible values: en_GB (English), de_DE (Deutsch),
es_CR (Español (Costa Rica)), es_ES (Español), eu
(Basque), fr_FR (Français), hr_HR (Croatian), it_IT
(Italiano), ja_JP (日本語), nl_BE (Nederlands), pt_BR
(Português (Brasil)), ru_RU (Русский), zh_CN (中文
(简体)).
Default: "en_GB"
dateFormat Date format.
@@ -225,7 +302,7 @@ The following commands are available in [command-line mode](#command-line-mode):
Possible values: HH:mm (20:30), h:mm A (8:30 PM).
Default: "HH:mm"
uncompletedTodosOnTop Show uncompleted todos on top of the lists.
uncompletedTodosOnTop Show uncompleted to-dos on top of the lists.
Type: bool.
Default: true
@@ -241,13 +318,37 @@ The following commands are available in [command-line mode](#command-line-mode):
Default: 300
sync.target Synchronisation target.
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).
Type: Enum.
Possible values: 2 (File system), 3 (OneDrive), 4
(OneDrive Dev (For testing only)).
(OneDrive Dev (For testing only)), 5 (Nextcloud), 6
(WebDAV).
Default: 3
sync.2.path Directory to synchronise with (absolute path).
The path to synchronise with when file system
synchronisation is enabled. See `sync.target`.
Type: string.
sync.5.path Nexcloud WebDAV URL.
Type: string.
sync.5.username Nexcloud username.
Type: string.
sync.5.password Nexcloud password.
Type: string.
sync.6.path WebDAV URL.
Type: string.
sync.6.username WebDAV username.
Type: string.
sync.6.password WebDAV password.
Type: string.
cp <note> [notebook]
@@ -258,14 +359,21 @@ The following commands are available in [command-line mode](#command-line-mode):
Marks a to-do as done.
e2ee <command> [path]
Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`,
`status` and `target-status`.
-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
edit <note>
Edit note.
exit
Exits the application.
export <directory>
Exports Joplin data to the given directory. By default, it will export the
@@ -320,9 +428,18 @@ The following commands are available in [command-line mode](#command-line-mode):
-f, --force Deletes the notes without asking for confirmation.
search <pattern> [notebook]
set <note> <name> [value]
Searches for the given <pattern> in all the notes.
Sets the property <name> of the given <note> to the given [value].
Possible properties are:
parent_id (text), title (text), body (text), created_time (int),
updated_time (int), is_conflict (int), latitude (numeric), longitude
(numeric), altitude (numeric), author (text), source_url (text), is_todo
(int), todo_due (int), todo_completed (int), source (text),
source_application (text), application_data (text), order (int),
user_created_time (int), user_updated_time (int), encryption_cipher_text
(text), encryption_applied (int)
status
@@ -334,7 +451,6 @@ The following commands are available in [command-line mode](#command-line-mode):
--target <target> Sync to provided target (defaults to sync.target config
value)
--random-failures For debugging purposes. Do not use.
tag <tag-command> [tag] [note]
@@ -353,6 +469,11 @@ The following commands are available in [command-line mode](#command-line-mode):
Marks a to-do as non-completed.
use <notebook>
Switches to [notebook] - all further operations will happen within this
notebook.
version
Displays version information

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 90
versionName "0.10.75"
versionCode 2097276
versionName "1.0.98"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.10.9</string>
<string>1.0.12</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>9</string>
<string>12</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@@ -13,4 +13,28 @@ ArrayUtils.removeElement = function(array, element) {
return array;
}
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
module.exports = ArrayUtils;

View File

@@ -26,12 +26,16 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
class BaseApplication {

View File

@@ -2,6 +2,7 @@ const { Log } = require('lib/log.js');
const { Database } = require('lib/database.js');
const { uuid } = require('lib/uuid.js');
const { time } = require('lib/time-utils.js');
const Mutex = require('async-mutex').Mutex;
class BaseModel {
@@ -247,6 +248,40 @@ class BaseModel {
return !Object.getOwnPropertyNames(diff).length;
}
static saveMutex(modelOrId) {
const noLockMutex = {
acquire: function() { return null; }
};
if (!modelOrId) return noLockMutex;
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
if (!modelId) return noLockMutex;
let mutex = BaseModel.saveMutexes_[modelId];
if (mutex) return mutex;
mutex = new Mutex();
BaseModel.saveMutexes_[modelId] = mutex;
return mutex;
}
static releaseSaveMutex(modelOrId, release) {
if (!release) return;
if (!modelOrId) return release();
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
if (!modelId) return release();
let mutex = BaseModel.saveMutexes_[modelId];
if (!mutex) return release();
delete BaseModel.saveMutexes_[modelId];
release();
}
static saveQuery(o, options) {
let temp = {}
let fieldNames = this.fieldNames();
@@ -320,7 +355,16 @@ class BaseModel {
return query;
}
static save(o, options = null) {
static async save(o, options = null) {
// When saving, there's a mutex per model ID. This is because the model returned from this function
// is basically its input `o` (instead of being read from the database, for performance reasons).
// This works well in general except if that model is saved simultaneously in two places. In that
// case, the output won't be up-to-date and would cause for example display issues with out-dated
// notes being displayed. This was an issue when notes were being synchronised while being decrypted
// at the same time.
const mutexRelease = await this.saveMutex(o).acquire();
options = this.modOptions(options);
options.isNew = this.isNew(o, options);
@@ -348,7 +392,11 @@ class BaseModel {
queries = queries.concat(options.nextQueries);
}
return this.db().transactionExecBatch(queries).then(() => {
let output = null;
try {
await this.db().transactionExecBatch(queries);
o = Object.assign({}, o);
if (modelId) o.id = modelId;
if ('updated_time' in saveQuery.modObject) o.updated_time = saveQuery.modObject.updated_time;
@@ -365,10 +413,14 @@ class BaseModel {
}
}
return this.filter(o);
}).catch((error) => {
output = this.filter(o);
} catch (error) {
Log.error('Cannot save model', error);
});
}
this.releaseSaveMutex(o, mutexRelease);
return output;
}
static isNew(object, options) {
@@ -447,5 +499,6 @@ BaseModel.TYPE_MASTER_KEY = 9;
BaseModel.db_ = null;
BaseModel.dispatch = function(o) {};
BaseModel.saveMutexes_ = {};
module.exports = BaseModel;

View File

@@ -10,6 +10,10 @@ class BaseSyncTarget {
this.options_ = options;
}
static supportsConfigCheck() {
return false;
}
option(name, defaultValue = null) {
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
}
@@ -30,6 +34,10 @@ class BaseSyncTarget {
return false;
}
authRouteName() {
return null;
}
static id() {
throw new Error('id() not implemented');
}

View File

@@ -2,11 +2,7 @@ class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code_ = code;
}
get code() {
return this.code_;
this.code = code;
}
}

View File

@@ -5,6 +5,7 @@ const Resource = require('lib/models/Resource.js');
const ModelCache = require('lib/ModelCache');
const { shim } = require('lib/shim.js');
const md5 = require('md5');
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
class MdToHtml {
@@ -28,7 +29,7 @@ class MdToHtml {
const r = resources[n];
k.push(r.id);
}
k.push(md5(body));
k.push(md5(escape(body))); // https://github.com/pvorb/node-md5/issues/41
k.push(md5(JSON.stringify(style)));
k.push(md5(JSON.stringify(options)));
return k.join('_');
@@ -73,7 +74,7 @@ class MdToHtml {
renderImage_(attrs, options) {
const loadResource = async (id) => {
console.info('Loading resource: ' + id);
// console.info('Loading resource: ' + id);
// Initially set to to an empty object to make
// it clear that it is being loaded. Otherwise
@@ -116,7 +117,7 @@ class MdToHtml {
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
let src = './' + Resource.filename(resource);
if (this.resourceBaseUrl_ !== null) src = this.resourceBaseUrl_ + src;
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
let output = '<img data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + src + '"/>';
return output;
}
@@ -156,27 +157,57 @@ class MdToHtml {
}
}
renderTokens_(tokens, options) {
rendererPlugin_(language) {
if (!language) return null;
const handlers = {};
handlers['katex'] = new MdToHtml_Katex();
return language in handlers ? handlers[language] : null;
}
parseInlineCodeLanguage_(content) {
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
if (m && m.length >= 2) {
const language = m[1];
return {
language: language,
newContent: content.substr(language.length + 3),
};
}
return null;
}
renderTokens_(markdownIt, tokens, options) {
let output = [];
let previousToken = null;
let anchorAttrs = [];
let extraCssBlocks = {};
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
let t = tokens[i];
const nextToken = i < tokens.length ? tokens[i+1] : null;
let tag = t.tag;
let openTag = null;
let closeTag = null;
let attrs = t.attrs ? t.attrs : [];
let tokenContent = t.content ? t.content : null;
const isCodeBlock = tag === 'code' && t.block;
const isInlineCode = t.type === 'code_inline';
const codeBlockLanguage = t && t.info ? t.info : null;
let rendererPlugin = null;
let rendererPluginOptions = { tagType: 'inline' };
// if (t.map) attrs.push(['data-map', t.map.join(':')]);
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
// Markdown-it render list items as <li><p>Text<p></li> which makes it
// complicated to style and layout the HTML, so we remove this extra
// <p> here and below in closeTag.
openTag = null;
} else if (isInlineCode) {
openTag = null;
} else if (tag && t.type.indexOf('_open') >= 0) {
openTag = tag;
} else if (tag && t.type.indexOf('_close') >= 0) {
@@ -186,7 +217,11 @@ class MdToHtml {
} else if (t.type === 'link_open') {
openTag = 'a';
} else if (isCodeBlock) {
openTag = 'pre';
if (rendererPlugin) {
openTag = null;
} else {
openTag = 'pre';
}
}
if (openTag) {
@@ -201,12 +236,35 @@ class MdToHtml {
if (isCodeBlock) {
const codeAttrs = ['code'];
if (t.info) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
if (!rendererPlugin) {
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
}
} else if (isInlineCode) {
const result = this.parseInlineCodeLanguage_(tokenContent);
if (result) {
rendererPlugin = this.rendererPlugin_(result.language);
tokenContent = result.newContent;
}
if (!rendererPlugin) {
output.push('<code>');
}
}
if (t.type === 'math_inline' || t.type === 'math_block') {
rendererPlugin = this.rendererPlugin_('katex');
rendererPluginOptions = { tagType: t.type === 'math_block' ? 'block' : 'inline' };
}
if (rendererPlugin) {
rendererPlugin.loadAssets().catch((error) => {
console.warn('MdToHtml: Error loading assets for ' + rendererPlugin.name() + ': ', error.message);
});
}
if (t.type === 'image') {
if (t.content) attrs.push(['title', t.content]);
if (tokenContent) attrs.push(['title', tokenContent]);
output.push(this.renderImage_(attrs, options));
} else if (t.type === 'softbreak') {
output.push('<br/>');
@@ -214,11 +272,17 @@ class MdToHtml {
output.push('<hr/>');
} else {
if (t.children) {
const parsedChildren = this.renderTokens_(t.children, options);
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
output = output.concat(parsedChildren);
} else {
if (t.content) {
output.push(htmlentities(t.content));
if (tokenContent) {
if ((isCodeBlock || isInlineCode) && rendererPlugin) {
output = rendererPlugin.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline');
} else if (rendererPlugin) {
output = rendererPlugin.processContent(output, tokenContent, rendererPluginOptions.tagType);
} else {
output.push(htmlentities(tokenContent));
}
}
}
}
@@ -230,10 +294,18 @@ class MdToHtml {
} else if (tag && t.type.indexOf('inline') >= 0) {
closeTag = openTag;
} else if (isCodeBlock) {
closeTag = openTag;
if (!rendererPlugin) closeTag = openTag;
}
if (isCodeBlock) output.push('</code>');
if (isCodeBlock) {
if (!rendererPlugin) {
output.push('</code>');
}
} else if (isInlineCode) {
if (!rendererPlugin) {
output.push('</code>');
}
}
if (closeTag) {
if (closeTag === 'a') {
@@ -243,8 +315,28 @@ class MdToHtml {
}
}
if (rendererPlugin) {
const extraCss = rendererPlugin.extraCss();
const name = rendererPlugin.name();
if (extraCss && !(name in extraCssBlocks)) {
extraCssBlocks[name] = extraCss;
}
}
previousToken = t;
}
// Insert the extra CSS at the top of the HTML
const temp = ['<style>'];
for (let n in extraCssBlocks) {
if (!extraCssBlocks.hasOwnProperty(n)) continue;
temp.push(extraCssBlocks[n]);
}
temp.push('</style>');
output = temp.concat(output);
return output.join('');
}
@@ -260,7 +352,13 @@ class MdToHtml {
breaks: true,
linkify: true,
});
const env = {};
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
// to math_inline and math_block blocks. These blocks are then processed directly with the Katex
// library. It is better this way as then it is possible to conditionally load the CSS required by
// Katex and use an up-to-date version of Katex (as of 2018, the plugin is still using 0.6, which is
// buggy instead of 0.9).
md.use(require('markdown-it-katex'));
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
@@ -278,12 +376,14 @@ class MdToHtml {
}
}
const env = {};
const tokens = md.parse(body, env);
let renderedBody = this.renderTokens_(md, tokens, options);
// console.info(body);
// console.info(tokens);
let renderedBody = this.renderTokens_(tokens, options);
// console.info(renderedBody);
if (HORRIBLE_HACK) {
let loopCount = 0;
@@ -374,9 +474,14 @@ class MdToHtml {
width: auto;
max-width: 100%;
}
.katex .mfrac .frac-line:before {
/* top: 50%; */
/* padding-bottom: .7em; */
}
`;
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; //+ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css">';
const output = styleHtml + renderedBody;

View File

@@ -0,0 +1,48 @@
const { shim } = require('lib/shim');
const katex = require('katex');
const katexCss = require('lib/csstojs/katex.css.js');
const Setting = require('lib/models/Setting');
class MdToHtml_Katex {
name() {
return 'katex';
}
processContent(renderedTokens, content, tagType) {
try {
let renderered = katex.renderToString(content);
if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
renderedTokens.push(renderered);
} catch (error) {
renderedTokens.push('Cannot render Katex content: ' + error.message);
}
return renderedTokens;
}
extraCss() {
return katexCss;
}
async loadAssets() {
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
if (shim.isReactNative()) {
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
const baseDir = Setting.value('resourceDir');
await shim.fsDriver().mkdir(baseDir + '/fonts');
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
}
}
}
module.exports = MdToHtml_Katex;

View File

@@ -0,0 +1,57 @@
// The Nextcloud sync target is essentially a wrapper over the WebDAV sync target,
// thus all the calls to SyncTargetWebDAV to avoid duplicate code.
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetNextcloud extends BaseSyncTarget {
static id() {
return 5;
}
static supportsConfigCheck() {
return true;
}
static targetName() {
return 'nextcloud';
}
static label() {
return _('Nextcloud');
}
isAuthenticated() {
return true;
}
static async checkConfig(options) {
return SyncTargetWebDAV.checkConfig(options);
}
async initFileApi() {
const fileApi = await SyncTargetWebDAV.initFileApi_(SyncTargetNextcloud.id(), {
path: Setting.value('sync.5.path'),
username: Setting.value('sync.5.username'),
password: Setting.value('sync.5.password'),
});
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetNextcloud;

View File

@@ -9,15 +9,15 @@ const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDrive extends BaseSyncTarget {
static id() {
return 3;
}
constructor(db, options = null) {
super(db, options);
this.api_ = null;
}
static id() {
return 3;
}
static targetName() {
return 'onedrive';
}
@@ -38,6 +38,10 @@ class SyncTargetOneDrive extends BaseSyncTarget {
return parameters().oneDrive;
}
authRouteName() {
return 'OneDriveLogin';
}
api() {
if (this.api_) return this.api_;

View File

@@ -12,6 +12,7 @@ class SyncTargetRegistry {
name: SyncTargetClass.targetName(),
label: SyncTargetClass.label(),
classRef: SyncTargetClass,
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
};
}
@@ -23,6 +24,18 @@ class SyncTargetRegistry {
throw new Error('Name not found: ' + name);
}
static idToMetadata(id) {
for (let n in this.reg_) {
if (!this.reg_.hasOwnProperty(n)) continue;
if (this.reg_[n].id === id) return this.reg_[n];
}
throw new Error('ID not found: ' + id);
}
static idToName(id) {
return this.idToMetadata(id).name;
}
static idAndLabelPlainObject() {
let output = {};
for (let n in this.reg_) {

View File

@@ -0,0 +1,83 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetWebDAV extends BaseSyncTarget {
static id() {
return 6;
}
static supportsConfigCheck() {
return true;
}
static targetName() {
return 'webdav';
}
static label() {
return _('WebDAV');
}
isAuthenticated() {
return true;
}
static async initFileApi_(syncTargetId, options) {
const apiOptions = {
baseUrl: () => options.path,
username: () => options.username,
password: () => options.password,
};
const api = new WebDavApi(apiOptions);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(syncTargetId);
return fileApi;
}
static async checkConfig(options) {
const fileApi = await SyncTargetWebDAV.initFileApi_(SyncTargetWebDAV.id(), options);
const output = {
ok: false,
errorMessage: '',
};
try {
const result = await fileApi.stat('');
if (!result) throw new Error('Could not access WebDAV directory');
output.ok = true;
} catch (error) {
output.errorMessage = error.message;
if (error.code) output.errorMessage += ' (Code ' + error.code + ')';
}
return output;
}
async initFileApi() {
const fileApi = await SyncTargetWebDAV.initFileApi_(SyncTargetWebDAV.id(), {
path: Setting.value('sync.6.path'),
username: Setting.value('sync.6.username'),
password: Setting.value('sync.6.password'),
});
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetWebDAV;

View File

@@ -0,0 +1,303 @@
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim.js');
const parseXmlString = require('xml2js').parseString;
const JoplinError = require('lib/JoplinError');
const URL = require('url-parse');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const base64 = require('base-64');
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
// implementations use the same namespaces. If not, extra processing can be done in `nameProcessor`, for
// example to convert a custom namespace to "d:" so that it can be used by the rest of the code.
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
class WebDavApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
authToken() {
if (!this.options_.username() || !this.options_.password()) return null;
return base64.encode(this.options_.username() + ':' + this.options_.password());
}
baseUrl() {
return this.options_.baseUrl();
}
relativeBaseUrl() {
const url = new URL(this.baseUrl());
return url.pathname + url.query;
}
async xmlToJson(xml) {
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
const nameProcessor = (name) => {
if (name.indexOf('xmlns:') !== 0) {
// Check if the current name is within the DAV namespace. If it is, normalise it
// by moving it to the "d:" namespace, which is what all the functions are using.
const p = name.split(':');
if (p.length == 2) {
const ns = p[0];
if (davNamespaces.indexOf(ns) >= 0) {
name = 'd:' + p[1];
}
}
}
return name.toLowerCase();
};
const attrValueProcessor = (value, name) => {
if (value.toLowerCase() === 'dav:') {
const p = name.split(':');
davNamespaces.push(p[p.length - 1]);
}
}
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
attrValueProcessors: [attrValueProcessor]
}
return new Promise((resolve, reject) => {
parseXmlString(xml, options, (error, result) => {
if (error) {
resolve(null); // Error handled by caller which will display the XML text (or plain text) if null is returned from this function
return;
}
resolve(result);
});
});
}
valueFromJson(json, keys, type) {
let output = json;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// console.info(key, typeof key, typeof output, typeof output === 'object' && (key in output), Array.isArray(output));
if (typeof key === 'number' && !Array.isArray(output)) return null;
if (typeof key === 'string' && (typeof output !== 'object' || !(key in output))) return null;
output = output[key];
}
if (type === 'string') {
// If the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
if (type === 'object') {
if (!Array.isArray(output) && typeof output === 'object') return output;
return null;
}
if (type === 'array') {
return Array.isArray(output) ? output : null;
}
return null;
}
stringFromJson(json, keys) {
return this.valueFromJson(json, keys, 'string');
}
objectFromJson(json, keys) {
return this.valueFromJson(json, keys, 'object');
}
arrayFromJson(json, keys) {
return this.valueFromJson(json, keys, 'array');
}
resourcePropByName(resource, outputType, propName) {
const propStats = resource['d:propstat'];
let output = null;
if (!Array.isArray(propStats)) throw new Error('Missing d:propstat property');
for (let i = 0; i < propStats.length; i++) {
const props = propStats[i]['d:prop'];
if (!Array.isArray(props) || !props.length) continue;
const prop = props[0];
if (Array.isArray(prop[propName])) {
output = prop[propName];
break;
}
}
if (outputType === 'string') {
// If the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
output = output[0];
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
if (outputType === 'array') {
return output;
}
throw new Error('Invalid output type: ' + outputType);
}
async execPropFind(path, depth, fields = null, options = null) {
if (fields === null) fields = ['d:getlastmodified'];
let fieldsXml = '';
for (let i = 0; i < fields.length; i++) {
fieldsXml += '<' + fields[i] + '/>';
}
// To find all available properties:
//
// const body=`<?xml version="1.0" encoding="utf-8" ?>
// <propfind xmlns="DAV:">
// <propname/>
// </propfind>`;
const body = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
<d:prop xmlns:oc="http://owncloud.org/ns">
` + fieldsXml + `
</d:prop>
</d:propfind>`;
return this.exec('PROPFIND', path, body, { 'Depth': depth }, options);
}
requestToCurl_(url, options) {
let output = [];
output.push('curl');
if (options.method) output.push('-X ' + options.method);
if (options.headers) {
for (let n in options.headers) {
if (!options.headers.hasOwnProperty(n)) continue;
output.push('-H ' + '"' + n + ': ' + options.headers[n] + '"');
}
}
if (options.body) output.push('--data ' + "'" + options.body + "'");
output.push(url);
return output.join(' ');
}
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
// <d:propfind xmlns:d="DAV:">
// <d:prop xmlns:oc="http://owncloud.org/ns">
// <d:getlastmodified/>
// </d:prop>
// </d:propfind>'
async exec(method, path = '', body = null, headers = null, options = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = 'json';
if (!options.target) options.target = 'string';
const authToken = this.authToken();
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
const fetchOptions = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
const url = this.baseUrl() + '/' + path;
let response = null;
// console.info('WebDAV Call', method + ' ' + url, headers, options);
// console.info(this.requestToCurl_(url, fetchOptions));
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
if (fetchOptions.path) {
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
if (fileStat) fetchOptions.headers['Content-Length'] = fileStat.size + '';
}
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = shim.stringByteLength(body) + '';
response = await shim.fetch(url, fetchOptions);
} else { // file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// console.info('WebDAV Response', responseText);
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = () => {
return (responseText + '').substr(0, 1024);
}
let responseJson_ = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
responseJson_ = await this.xmlToJson(responseText);
if (!responseJson_) throw new JoplinError('Cannot parse JSON response: ' + shortResponseText(), response.status);
return responseJson_;
}
if (!response.ok) {
// When using fetchBlob we only get a string (not xml or json) back
if (options.target === 'file') throw new JoplinError(shortResponseText(), response.status);
const json = await loadResponseJson();
if (json && json['d:error']) {
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : shortResponseText();
throw new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + ')', response.status);
}
throw new JoplinError(method + ' ' + path + ': ' + shortResponseText(), response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
// null responses are possible, for example for DELETE calls
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw new Error('Not a valid JSON response: ' + shortResponseText());
return output;
}
}
module.exports = WebDavApi;

View File

@@ -2,7 +2,7 @@ const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux');
const { NotesScreen } = require('lib/components/screens/notes.js');
const { SearchScreen } = require('lib/components/screens/search.js');
const { View } = require('react-native');
const { KeyboardAvoidingView, Keyboard, Platform, View } = require('react-native');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/components/global-style.js');
@@ -11,6 +11,31 @@ class AppNavComponent extends Component {
constructor() {
super();
this.previousRouteName_ = null;
this.state = {
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
}
}
componentWillMount() {
if (Platform.OS === 'ios') {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
}
}
componentWillUnmount() {
if (this.keyboardDidShowListener) this.keyboardDidShowListener.remove();
if (this.keyboardDidHideListener) this.keyboardDidHideListener.remove();
this.keyboardDidShowListener = null;
this.keyboardDidHideListener = null;
}
keyboardDidShow () {
this.setState({ autoCompletionBarExtraHeight: 30 })
}
keyboardDidHide () {
this.setState({ autoCompletionBarExtraHeight:0 })
}
render() {
@@ -44,11 +69,12 @@ class AppNavComponent extends Component {
const style = { flex: 1, backgroundColor: theme.backgroundColor }
return (
<View style={style}>
<KeyboardAvoidingView behavior={ Platform.OS === 'ios' ? "padding" : null } style={style}>
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
{ searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} /> }
{ (!notesScreenVisible && !searchScreenVisible) && <Screen navigation={{ state: route }} /> }
</View>
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
</KeyboardAvoidingView>
);
}

View File

@@ -13,7 +13,7 @@ const globalStyle = {
fontSizeSmaller: 14,
dividerColor: "#dddddd",
selectedColor: '#e5e5e5',
disabledOpacity: 0.3,
disabledOpacity: 0.2,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",

View File

@@ -51,7 +51,19 @@ class NoteBodyViewer extends Component {
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
};
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
html = `
<!DOCTYPE html>
<html>
<head>
</head>
<body>
` + html + `
</body>
</html>
`;
let webViewStyle = {}
// On iOS, the onLoadEnd() event is never fired so always

View File

@@ -372,7 +372,7 @@ class ScreenHeaderComponent extends Component {
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
output.push({ label: Folder.displayTitle(f), value: f.id });
}
output.sort((a, b) => {
if (a.value === null) return -1;

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { Platform, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
const { _, setLocale } = require('lib/locale.js');
@@ -7,6 +7,8 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { Dropdown } = require('lib/components/Dropdown.js');
const { themeStyle } = require('lib/components/global-style.js');
const Setting = require('lib/models/Setting.js');
const shared = require('lib/components/shared/config-shared.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class ConfigScreenComponent extends BaseScreenComponent {
@@ -17,6 +19,20 @@ class ConfigScreenComponent extends BaseScreenComponent {
constructor() {
super();
this.styles_ = {};
shared.init(this);
this.checkSyncConfig_ = async () => {
await shared.checkSyncConfig(this, this.state.settings);
}
this.saveButton_press = () => {
return shared.saveSettings(this);
};
}
componentWillMount() {
this.setState({ settings: this.props.settings });
}
styles() {
@@ -49,12 +65,22 @@ class ConfigScreenComponent extends BaseScreenComponent {
fontSize: theme.fontSize,
flex: 1,
},
descriptionText: {
color: theme.color,
fontSize: theme.fontSize,
flex: 1,
},
settingControl: {
color: theme.color,
flex: 1,
},
}
if (Platform.OS === 'ios') {
styles.settingControl.borderBottomWidth = 1;
styles.settingControl.borderBottomColor = theme.dividerColor;
}
styles.switchSettingText = Object.assign({}, styles.settingText);
styles.switchSettingText.width = '80%';
@@ -83,7 +109,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
let output = null;
const updateSettingValue = (key, value) => {
Setting.setValue(key, value);
return shared.updateSettingValue(this, key, value);
}
const md = Setting.settingMetadata(key);
@@ -135,26 +161,44 @@ class ConfigScreenComponent extends BaseScreenComponent {
<Slider key="control" style={this.styles().settingControl} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
</View>
);
} else if (md.type == Setting.TYPE_STRING) {
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
<TextInput autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
);
} else {
//throw new Error('Unsupported setting type: ' + setting.type);
//throw new Error('Unsupported setting type: ' + md.type);
}
return output;
}
render() {
const settings = this.props.settings;
const settings = this.state.settings;
const keys = Setting.keys(true, 'mobile');
let settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(key)) continue;
const settingComps = shared.settingsToComponents(this, 'mobile', settings);
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusComp = !messages.length ? null : (
<View style={{flex:1, marginTop: 10}}>
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
{messages.length >= 1 ? (<View style={{marginTop:10}}><Text style={this.styles().descriptionText}>{messages[1]}</Text></View>) : null}
</View>);
settingComps.push(
<View key="check_sync_config_button" style={this.styles().settingContainer}>
<View style={{flex:1, flexDirection: 'column'}}>
<View style={{flex:1}}>
<Button title={_('Check synchronisation configuration')} onPress={this.checkSyncConfig_}/>
</View>
{ statusComp }
</View>
</View>);
}
settingComps.push(
@@ -173,11 +217,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
);
//style={this.styles().body}
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Configuration')}/>
<ScreenHeader
title={_('Configuration')}
showSaveButton={true}
saveButtonDisabled={!this.state.changedSettingKeys.length}
onSaveButtonPress={this.saveButton_press}
/>
<ScrollView >
{ settingComps }
</ScrollView>

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TextInput, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { TextInput, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, Platform } = require('react-native');
const EncryptionService = require('lib/services/EncryptionService');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
@@ -109,13 +109,20 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
const inputStyle = {flex:1, marginRight: 10, color: theme.color};
if (Platform.OS === 'ios') {
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
}
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0,6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Text style={{flex:0, fontSize: theme.fontSize, marginRight: 10, color: theme.color}}>{_('Password:')}</Text>
<TextInput secureTextEntry={true} value={password} onChangeText={(text) => onPasswordChange(text)} style={{flex:1, marginRight: 10, color: theme.color}}></TextInput>
<TextInput secureTextEntry={true} value={password} onChangeText={(text) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{fontSize: theme.fontSize, marginRight: 10, color: theme.color}}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
@@ -159,9 +166,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@@ -183,6 +196,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{marginTop: 10}}>{rows}</View>
</View>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
@@ -191,12 +222,12 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
<ScreenHeader title={_('Encryption Config')}/>
<ScrollView style={this.styles().container}>
<View style={{backgroundColor: theme.warningBackgroundColor, padding: 5}}>
{/*<View style={{backgroundColor: theme.warningBackgroundColor, padding: 5}}>
<Text>Important: This is a *beta* feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain.</Text>
<Text>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 your notes from the desktop or terminal application.</Text>
<Text>For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation:</Text>
<TouchableOpacity onPress={() => { Linking.openURL('http://joplin.cozic.net/help/e2ee.html') }}><Text>http://joplin.cozic.net/help/e2ee.html</Text></TouchableOpacity>
</View>
</View>*/}
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
@@ -204,6 +235,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{flex:1, height: 20}}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
@@ -221,6 +253,7 @@ const EncryptionConfigScreen = connect(
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
}
)(EncryptionConfigScreenComponent)

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { ListView, View, Text, Button, StyleSheet } = require('react-native');
const { ListView, View, Text, Button, StyleSheet, Platform } = require('react-native');
const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const { reg } = require('lib/registry.js');
@@ -23,6 +23,7 @@ class LogScreenComponent extends BaseScreenComponent {
});
this.state = {
dataSource: ds,
showErrorsOnly: false,
};
this.styles_ = {};
}
@@ -42,12 +43,15 @@ class LogScreenComponent extends BaseScreenComponent {
paddingBottom:0,
},
rowText: {
fontFamily: 'monospace',
fontSize: 10,
color: theme.color,
},
};
if (Platform.OS !== 'ios') { // Crashes on iOS with error "Unrecognized font family 'monospace'"
styles.rowText.fontFamily = 'monospace';
}
styles.rowTextError = Object.assign({}, styles.rowText);
styles.rowTextError.color = theme.colorError;
@@ -62,13 +66,24 @@ class LogScreenComponent extends BaseScreenComponent {
this.resfreshLogEntries();
}
resfreshLogEntries() {
reg.logger().lastEntries(1000).then((entries) => {
resfreshLogEntries(showErrorsOnly = null) {
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
}
toggleErrorsOnly() {
const showErrorsOnly = !this.state.showErrorsOnly;
this.setState({ showErrorsOnly: showErrorsOnly });
this.resfreshLogEntries(showErrorsOnly);
}
render() {
let renderRow = (item) => {
let textStyle = this.styles().rowText;
@@ -91,7 +106,14 @@ class LogScreenComponent extends BaseScreenComponent {
renderRow={renderRow}
enableEmptySections={true}
/>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1, marginRight: 5 }}>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
</View>
<View style={{flex:1}}>
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
</View>
</View>
</View>
);
}

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, KeyboardAvoidingView } = require('react-native');
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
const { connect } = require('react-redux');
const { uuid } = require('lib/uuid.js');
const { Log } = require('lib/log.js');
@@ -149,12 +149,6 @@ class NoteScreenComponent extends BaseScreenComponent {
await shared.initState(this);
this.refreshNoteMetadata();
if (Platform.OS === 'ios') {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
}
}
refreshNoteMetadata(force = null) {
@@ -163,19 +157,6 @@ class NoteScreenComponent extends BaseScreenComponent {
componentWillUnmount() {
BackButtonService.removeHandler(this.backHandler);
if (Platform.OS === 'ios'){
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
}
_keyboardDidShow () {
this.setState({ heightBumpView:30 })
}
_keyboardDidHide () {
this.setState({ heightBumpView:0 })
}
title_changeText(text) {
@@ -542,7 +523,7 @@ class NoteScreenComponent extends BaseScreenComponent {
);
return (
<KeyboardAvoidingView behavior= {(Platform.OS === 'ios')? "padding" : null} style={this.rootStyle(this.props.theme).root}>
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader
folderPickerOptions={{
enabled: true,
@@ -578,8 +559,7 @@ class NoteScreenComponent extends BaseScreenComponent {
/>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<View style={{ height: this.state.heightBumpView }} />
</KeyboardAvoidingView>
</View>
);
}

View File

@@ -0,0 +1,81 @@
const Setting = require('lib/models/Setting.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const { _ } = require('lib/locale.js');
const shared = {}
shared.init = function(comp) {
if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null;
comp.state.settings = {};
comp.state.changedSettingKeys = [];
}
shared.checkSyncConfig = async function(comp, settings) {
const syncTargetId = settings['sync.target'];
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
const options = Setting.subValues('sync.' + syncTargetId, settings);
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(options);
comp.setState({ checkSyncConfigResult: result });
}
shared.checkSyncConfigMessages = function(comp) {
const result = comp.state.checkSyncConfigResult;
const output = [];
if (result === 'checking') {
output.push(_('Checking... Please wait.'));
} else if (result && result.ok) {
output.push(_('Success! Synchronisation configuration appears to be correct.'));
} else if (result && !result.ok) {
output.push(_('Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:'));
output.push(result.errorMessage);
}
return output;
}
shared.updateSettingValue = function(comp, key, value) {
const settings = Object.assign({}, comp.state.settings);
const changedSettingKeys = comp.state.changedSettingKeys.slice();
settings[key] = Setting.formatValue(key, value);
if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key);
comp.setState({
settings: settings,
changedSettingKeys: changedSettingKeys,
});
}
shared.saveSettings = function(comp) {
for (let key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue;
if (comp.state.changedSettingKeys.indexOf(key) < 0) continue;
console.info("Saving", key, comp.state.settings[key]);
Setting.setValue(key, comp.state.settings[key]);
}
comp.setState({ changedSettingKeys: [] });
}
shared.settingsToComponents = function(comp, device, settings) {
const keys = Setting.keys(true, device);
const settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!Setting.isPublic(key)) continue;
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const settingComp = comp.settingToComponent(key, settings[key]);
if (!settingComp) continue;
settingComps.push(settingComp);
}
return settingComps
}
module.exports = shared;

View File

@@ -63,11 +63,33 @@ shared.saveNoteButton_press = async function(comp) {
note: note,
};
if (isNew) newState.newAndNoTitleChangeNoteId = note.id;
if (isNew && hasAutoTitle) newState.newAndNoTitleChangeNoteId = note.id;
comp.setState(newState);
if (isNew) Note.updateGeolocation(note.id);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
// Geo-location for this note has been saved to the database however the properties
// are is not in the state so set them now.
const geoInfo = {
longitude: geoNote.longitude,
latitude: geoNote.latitude,
altitude: geoNote.altitude,
}
const modNote = Object.assign({}, stateNote, geoInfo);
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
comp.refreshNoteMetadata();
});
}
comp.refreshNoteMetadata();
if (isNew) {

View File

@@ -36,12 +36,17 @@ shared.synchronize_press = async function(comp) {
const action = comp.props.syncStarted ? 'cancel' : 'start';
if (!reg.syncTarget().isAuthenticated()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: 'OneDriveLogin',
});
return 'auth';
if (!reg.syncTarget().isAuthenticated()) {
if (reg.syncTarget().authRouteName()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: reg.syncTarget().authRouteName(),
});
return 'auth';
}
reg.logger().info('Not authentified with sync target - please check your credential.');
return 'error';
}
let sync = null;

View File

@@ -1,5 +1,5 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
@@ -42,10 +42,8 @@ class FileApiDriverLocal {
metadataFromStat_(stat) {
return {
path: stat.path,
created_time: stat.birthtime.getTime(),
// created_time: stat.birthtime.getTime(),
updated_time: stat.mtime.getTime(),
created_time_orig: stat.birthtime,
updated_time_orig: stat.mtime,
isDir: stat.isDirectory(),
};
}
@@ -68,41 +66,14 @@ class FileApiDriverLocal {
}
async delta(path, options) {
const itemIds = await options.allItemIdsHandler();
const getStatFn = async (path) => {
const stats = await this.fsDriver().readDirStats(path);
return this.metadataFromStats_(stats);
};
try {
const stats = await this.fsDriver().readDirStats(path);
let output = this.metadataFromStats_(stats);
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
let found = false;
for (let j = 0; j < output.length; j++) {
const item = output[j];
if (BaseItem.pathToId(item.path) == itemId) {
found = true;
break;
}
}
if (!found) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
return {
hasMore: false,
context: null,
items: output,
};
const output = await basicDelta(path, getStatFn, options);
return output;
} catch(error) {
throw this.fsErrorToJsError_(error, path);
}
@@ -253,6 +224,11 @@ class FileApiDriverLocal {
throw new Error('Not supported');
}
async clearRoot(baseDir) {
await this.fsDriver().remove(baseDir);
await this.fsDriver().mkdir(baseDir);
}
}
module.exports = { FileApiDriverLocal };

View File

@@ -1,5 +1,6 @@
const { time } = require('lib/time-utils.js');
const fs = require('fs-extra');
const { basicDelta } = require('lib/file-api');
class FileApiDriverMemory {
@@ -17,7 +18,7 @@ class FileApiDriverMemory {
}
decodeContent_(content) {
return Buffer.from(content, 'base64').toString('ascii');
return Buffer.from(content, 'base64').toString('utf-8');
}
itemIndexByPath(path) {
@@ -38,7 +39,7 @@ class FileApiDriverMemory {
path: path,
isDir: isDir,
updated_time: now, // In milliseconds!!
created_time: now, // In milliseconds!!
// created_time: now, // In milliseconds!!
content: '',
};
}
@@ -48,14 +49,13 @@ class FileApiDriverMemory {
return Promise.resolve(item ? Object.assign({}, item) : null);
}
setTimestamp(path, timestampMs) {
async setTimestamp(path, timestampMs) {
let item = this.itemByPath(path);
if (!item) return Promise.reject(new Error('File not found: ' + path));
item.updated_time = timestampMs;
return Promise.resolve();
}
list(path, options) {
async list(path, options) {
let output = [];
for (let i = 0; i < this.items_.length; i++) {
@@ -94,11 +94,10 @@ class FileApiDriverMemory {
return output;
}
mkdir(path) {
async mkdir(path) {
let index = this.itemIndexByPath(path);
if (index >= 0) return Promise.resolve();
if (index >= 0) return;
this.items_.push(this.newItem(path, true));
return Promise.resolve();
}
async put(path, content, options = null) {
@@ -115,10 +114,9 @@ class FileApiDriverMemory {
this.items_[index].content = this.encodeContent_(content);
this.items_[index].updated_time = time.unix();
}
return Promise.resolve();
}
delete(path) {
async delete(path) {
let index = this.itemIndexByPath(path);
if (index >= 0) {
let item = Object.assign({}, this.items_[index]);
@@ -127,68 +125,38 @@ class FileApiDriverMemory {
this.deletedItems_.push(item);
this.items_.splice(index, 1);
}
return Promise.resolve();
}
move(oldPath, newPath) {
async move(oldPath, newPath) {
let sourceItem = this.itemByPath(oldPath);
if (!sourceItem) return Promise.reject(new Error('Path not found: ' + oldPath));
this.delete(newPath); // Overwrite if newPath already exists
sourceItem.path = newPath;
return Promise.resolve();
}
format() {
async format() {
this.items_ = [];
return Promise.resolve();
}
async delta(path, options = null) {
let limit = 3;
let output = {
hasMore: false,
context: {},
items: [],
const getStatFn = async (path) => {
let output = this.items_.slice();
for (let i = 0; i < output.length; i++) {
const item = Object.assign({}, output[i]);
item.path = item.path.substr(path.length + 1);
output[i] = item;
}
return output;
};
let context = options ? options.context : null;
let fromTime = 0;
if (context) fromTime = context.fromTime;
let sortedItems = this.items_.slice().concat(this.deletedItems_);
sortedItems.sort((a, b) => {
if (a.updated_time < b.updated_time) return -1;
if (a.updated_time > b.updated_time) return +1;
return 0;
});
let hasMore = false;
let items = [];
let maxTime = 0;
for (let i = 0; i < sortedItems.length; i++) {
let item = sortedItems[i];
if (item.updated_time >= fromTime) {
item = Object.assign({}, item);
item.path = item.path.substr(path.length + 1);
items.push(item);
if (item.updated_time > maxTime) maxTime = item.updated_time;
}
if (items.length >= limit) {
hasMore = true;
break;
}
}
output.items = items;
output.hasMore = hasMore;
output.context = { fromTime: maxTime };
const output = await basicDelta(path, getStatFn, options);
return output;
}
async clearRoot() {
this.items_ = [];
}
}
module.exports = { FileApiDriverMemory };

View File

@@ -41,7 +41,7 @@ class FileApiDriverOneDrive {
if ('deleted' in odItem) {
output.isDeleted = true;
} else {
output.created_time = moment(odItem.fileSystemInfo.createdDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
// output.created_time = moment(odItem.fileSystemInfo.createdDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
output.updated_time = moment(odItem.fileSystemInfo.lastModifiedDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
}
@@ -189,6 +189,10 @@ class FileApiDriverOneDrive {
return this.pathCache_[path];
}
clearRoot() {
throw new Error('Not implemented');
}
async delta(path, options = null) {
let output = {
hasMore: false,

View File

@@ -0,0 +1,354 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
constructor(api) {
this.api_ = api;
}
api() {
return this.api_;
}
requestRepeatCount() {
return 3;
}
async stat(path) {
try {
const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified',
'd:resourcetype',
// 'd:getcontentlength', // Remove this once PUT call issue is sorted out
]);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
return this.statFromResource_(resource, path);
} catch (error) {
if (error.code === 404) return null;
throw error;
}
}
statFromResource_(resource, path) {
// WebDAV implementations are always slighly different from one server to another but, at the minimum,
// a resource should have a propstat key - if not it's probably an error.
const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource));
const resourceTypes = this.api().resourcePropByName(resource, 'array', 'd:resourcetype');
let isDir = false;
if (Array.isArray(resourceTypes)) {
for (let i = 0; i < resourceTypes.length; i++) {
const t = resourceTypes[i];
if (typeof t === 'object' && 'd:collection' in t) {
isDir = true;
break;
}
}
}
const lastModifiedString = this.api().resourcePropByName(resource, 'string', 'd:getlastmodified');
// const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
// if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
// Note: Not all WebDAV servers return a getlastmodified date (eg. Seafile, which doesn't return the
// property for folders) so we can only throw an error if it's a file.
if (!lastModifiedString && !isDir) throw new Error('Could not get lastModified date for resource: ' + JSON.stringify(resource));
const lastModifiedDate = lastModifiedString ? new Date(lastModifiedString) : new Date();
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
return {
path: path,
// created_time: lastModifiedDate.getTime(),
updated_time: lastModifiedDate.getTime(),
isDir: isDir,
// sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
};
}
async setTimestamp(path, timestampMs) {
throw new Error('Not implemented'); // Not needed anymore
}
async delta(path, options) {
const getDirStats = async (path) => {
const result = await this.list(path);
return result.items;
};
return await basicDelta(path, getDirStats, options);
}
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
// works with paths relative to the base URL.
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
let output = '';
if (href.indexOf(baseUrl) === 0) {
output = href.substr(baseUrl.length);
} else if (href.indexOf(relativeBaseUrl) === 0) {
output = href.substr(relativeBaseUrl.length);
} else {
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
}
return rtrimSlashes(ltrimSlashes(output));
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
const baseUrl = this.api().baseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async list(path, options) {
// const relativeBaseUrl = this.api().relativeBaseUrl();
// function parsePropFindXml(xmlString) {
// return new Promise(async (resolve, reject) => {
// const saxOptions = {};
// const saxParser = require('sax').parser(false, { position: false });
// let stats = [];
// let currentStat = null;
// let currentText = '';
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // which makes parsing about 25% faster. However it's a bit of a hack so keep it as
// // an option so that it can be disabled if it causes problems.
// const optimizeXml = true;
// const tagResponse = optimizeXml ? 'd:r' : 'd:response';
// const tagGetLastModified = optimizeXml ? 'd:glm' : 'd:getlastmodified';
// const tagPropStat = optimizeXml ? 'd:ps' : 'd:propstat';
// const replaceUrls = optimizeXml;
// saxParser.onerror = function (error) {
// reject(new Error(e.toString()));
// };
// saxParser.ontext = function (t) {
// currentText += t;
// };
// saxParser.onopentag = function (node) {
// const tagName = node.name.toLowerCase();
// currentText = '';
// if (tagName === tagResponse) {
// currentStat = { isDir: false };
// }
// };
// saxParser.onclosetag = function(tagName) {
// tagName = tagName.toLowerCase();
// if (tagName === tagResponse) {
// if (currentStat.path) { // The list of resources includes the root dir too, which we don't want
// if (!currentStat.updated_time) throw new Error('Resource does not have a getlastmodified prop');
// stats.push(currentStat);
// }
// currentStat = null;
// }
// if (tagName === 'd:href') {
// const href = currentText;
// if (replaceUrls) {
// currentStat.path = rtrimSlashes(ltrimSlashes(href));
// } else {
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// currentStat.path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// }
// }
// if (tagName === tagGetLastModified) {
// const lastModifiedDate = new Date(currentText);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + currentText);
// currentStat.updated_time = lastModifiedDate.getTime();
// currentStat.created_time = currentStat.updated_time;
// }
// if (tagName === 'd:collection') {
// currentStat.isDir = true;
// }
// currentText = '';
// }
// saxParser.onend = function () {
// resolve(stats);
// };
// if (optimizeXml) {
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// }
// let idx = 0;
// let size = 1024 * 100;
// while (true) {
// sub = xmlString.substr(idx, size);
// if (!sub.length) break;
// saxParser.write(sub);
// idx += size;
// //await time.msleep(500);
// }
// saxParser.close();
// //saxParser.write(xmlString).close();
// });
// }
// For performance reasons, the response of the PROPFIND call is manually parsed with a regex below
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
// let output = [];
// let match = null;
// while (match = regex.exec(xmlString)) {
// const href = html_entity_decode(match[1]);
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
// const lastModifiedDate = new Date(match[2]);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
// output.push({
// path: path,
// updated_time: lastModifiedDate.getTime(),
// created_time: lastModifiedDate.getTime(),
// isDir: !BaseItem.isSystemPath(path),
// });
// }
// return output;
// }
// const resultXml = await this.api().execPropFind(path, 1, [
// 'd:getlastmodified',
// //'d:resourcetype', // Include this to use parsePropFindXml()
// ], { responseFormat: 'text' });
// const stats = await parsePropFindXml2(resultXml);
// return {
// items: stats,
// hasMore: false,
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources);
return {
items: stats,
hasMore: false,
context: null,
};
}
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
const response = await this.api().exec('GET', path, null, null, options);
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
// with a response body "The specified file doesn't exist." for non-existing files,
// so we need to check for this.
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
return response;
} catch (error) {
if (error.code !== 404) throw error;
}
}
async mkdir(path) {
try {
await this.api().exec('MKCOL', path);
} catch (error) {
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
// we get this code, verify that indeed the directory already exists and exit if it does.
if (error.code === 409) {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
async put(path, content, options = null) {
return await this.api().exec('PUT', path, content, null, options);
}
async delete(path) {
try {
await this.api().exec('DELETE', path);
} catch (error) {
if (error.code !== 404) throw error;
}
}
async move(oldPath, newPath) {
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
});
}
format() {
throw new Error('Not supported');
}
async clearRoot() {
await this.delete('');
await this.mkdir('');
}
}
module.exports = { FileApiDriverWebDav };

View File

@@ -1,5 +1,34 @@
const { isHidden } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');
const ArrayUtils = require('lib/ArrayUtils');
const { time } = require('lib/time-utils.js');
function requestCanBeRepeated(error) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
if (errorCode === 'rejectedByTarget') return false;
return true;
}
async function tryAndRepeat(fn, count) {
let retryCount = 0;
while (true) {
try {
const result = await fn();
return result;
} catch (error) {
if (retryCount >= count) throw error;
if (!requestCanBeRepeated(error)) throw error;
retryCount++;
await time.sleep(1 + retryCount * 3);
}
}
}
class FileApi {
@@ -8,6 +37,31 @@ class FileApi {
this.driver_ = driver;
this.logger_ = new Logger();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
}
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
// historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver,
// and it defaults to no repeating.
requestRepeatCount() {
if (this.requestRepeatCount_ !== null) return this.requestRepeatCount_;
if (this.driver_.requestRepeatCount) return this.driver_.requestRepeatCount();
return 0;
}
tempDirName() {
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
return this.tempDirName_;
}
setTempDirName(v) {
this.tempDirName_ = v;
}
fsDriver() {
return shim.fsDriver();
}
driver() {
@@ -24,6 +78,7 @@ class FileApi {
}
setLogger(l) {
if (!l) l = new Logger();
this.logger_ = l;
}
@@ -32,81 +87,230 @@ class FileApi {
}
fullPath_(path) {
let output = this.baseDir_;
if (path != '') output += '/' + path;
return output;
let output = [];
if (this.baseDir_) output.push(this.baseDir_);
if (path) output.push(path);
return output.join('/');
}
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
list(path = '', options = null) {
async list(path = '', options = null) {
if (!options) options = {};
if (!('includeHidden' in options)) options.includeHidden = false;
if (!('context' in options)) options.context = null;
this.logger().debug('list ' + this.baseDir_);
return this.driver_.list(this.baseDir_, options).then((result) => {
if (!options.includeHidden) {
let temp = [];
for (let i = 0; i < result.items.length; i++) {
if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
}
result.items = temp;
const result = await tryAndRepeat(() => this.driver_.list(this.baseDir_, options), this.requestRepeatCount());
if (!options.includeHidden) {
let temp = [];
for (let i = 0; i < result.items.length; i++) {
if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
}
return result;
});
result.items = temp;
}
return result;
// return this.driver_.list(this.baseDir_, options).then((result) => {
// if (!options.includeHidden) {
// let temp = [];
// for (let i = 0; i < result.items.length; i++) {
// if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
// }
// result.items = temp;
// }
// return result;
// });
}
// Deprectated
setTimestamp(path, timestampMs) {
this.logger().debug('setTimestamp ' + this.fullPath_(path));
return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath_(path), timestampMs), this.requestRepeatCount());
//return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
}
mkdir(path) {
this.logger().debug('mkdir ' + this.fullPath_(path));
return this.driver_.mkdir(this.fullPath_(path));
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath_(path)), this.requestRepeatCount());
}
stat(path) {
async stat(path) {
this.logger().debug('stat ' + this.fullPath_(path));
return this.driver_.stat(this.fullPath_(path)).then((output) => {
if (!output) return output;
output.path = path;
return output;
});
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath_(path)), this.requestRepeatCount());
if (!output) return output;
output.path = path;
return output;
// return this.driver_.stat(this.fullPath_(path)).then((output) => {
// if (!output) return output;
// output.path = path;
// return output;
// });
}
get(path, options = null) {
if (!options) options = {};
if (!options.encoding) options.encoding = 'utf8';
this.logger().debug('get ' + this.fullPath_(path));
return this.driver_.get(this.fullPath_(path), options);
return tryAndRepeat(() => this.driver_.get(this.fullPath_(path), options), this.requestRepeatCount());
}
put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path));
return this.driver_.put(this.fullPath_(path), content, options);
async put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path), options);
if (options && options.source === 'file') {
if (!await this.fsDriver().exists(options.path)) throw new JoplinError('File not found: ' + options.path, 'fileNotFound');
}
return tryAndRepeat(() => this.driver_.put(this.fullPath_(path), content, options), this.requestRepeatCount());
}
delete(path) {
this.logger().debug('delete ' + this.fullPath_(path));
return this.driver_.delete(this.fullPath_(path));
return tryAndRepeat(() => this.driver_.delete(this.fullPath_(path)), this.requestRepeatCount());
}
// Deprectated
move(oldPath, newPath) {
this.logger().debug('move ' + this.fullPath_(oldPath) + ' => ' + this.fullPath_(newPath));
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
return tryAndRepeat(() => this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)), this.requestRepeatCount());
}
// Deprectated
format() {
return this.driver_.format();
return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount());
}
clearRoot() {
return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir_), this.requestRepeatCount());
}
delta(path, options = null) {
this.logger().debug('delta ' + this.fullPath_(path));
return this.driver_.delta(this.fullPath_(path), options);
return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount());
}
}
module.exports = { FileApi };
function basicDeltaContextFromOptions_(options) {
let output = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
statIdsCache: null,
deletedItemsProcessed: false,
};
if (!options || !options.context) return output;
const d = new Date(options.context.timestamp);
output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && ('deletedItemsProcessed' in options.context) ? options.context.deletedItemsProcessed : false;
return output;
}
// This is the basic delta algorithm, which can be used in case the cloud service does not have
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
// the file system do not.
async function basicDelta(path, getDirStatFn, options) {
const outputLimit = 1000;
const itemIds = await options.allItemIdsHandler();
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
const context = basicDeltaContextFromOptions_(options);
let newContext = {
timestamp: context.timestamp,
filesAtTimestamp: context.filesAtTimestamp.slice(),
statsCache: context.statsCache,
statIdsCache: context.statIdsCache,
deletedItemsProcessed: context.deletedItemsProcessed,
};
// Stats are cached until all items have been processed (until hasMore is false)
if (newContext.statsCache === null) {
newContext.statsCache = await getDirStatFn(path);
newContext.statsCache.sort(function(a, b) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache.map((item) => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
let output = [];
// Find out which files have been changed since the last time. Note that we keep
// both the timestamp of the most recent change, *and* the items that exactly match
// this timestamp. This to handle cases where an item is modified while this delta
// function is running. For example:
// t0: Item 1 is changed
// t0: Sync items - run delta function
// t0: While delta() is running, modify Item 2
// Since item 2 was modified within the same millisecond, it would be skipped in the
// next sync if we relied exclusively on a timestamp.
for (let i = 0; i < newContext.statsCache.length; i++) {
const stat = newContext.statsCache[i];
if (stat.isDir) continue;
if (stat.updated_time < context.timestamp) continue;
// Special case for items that exactly match the timestamp
if (stat.updated_time === context.timestamp) {
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
}
if (stat.updated_time > newContext.timestamp) {
newContext.timestamp = stat.updated_time;
newContext.filesAtTimestamp = [];
}
newContext.filesAtTimestamp.push(stat.path);
output.push(stat);
if (output.length >= outputLimit) break;
}
if (!newContext.deletedItemsProcessed) {
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
// Note that when deleted items are processed it might result in the output having
// more items than outputLimit. This is acceptable since delete operations are cheap.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
}
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) newContext.statsCache = null;
return {
hasMore: hasMore,
context: newContext,
items: output,
};
}
module.exports = { FileApi, basicDelta };

View File

@@ -3,21 +3,50 @@ const { time } = require('lib/time-utils.js');
class FsDriverNode {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += '. Path: ' + path;
let output = new Error(msg);
if (error.code) output.code = error.code;
return output;
}
appendFileSync(path, string) {
return fs.appendFileSync(path, string);
}
appendFile(path, string, encoding = 'base64') {
return fs.appendFile(path, string, { encoding: encoding });
async appendFile(path, string, encoding = 'base64') {
try {
return await fs.appendFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeBinaryFile(path, content) {
let buffer = new Buffer(content);
return fs.writeFile(path, buffer);
async writeBinaryFile(path, content) {
try {
let buffer = new Buffer(content);
return await fs.writeFile(path, buffer);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeFile(path, string, encoding = 'base64') {
return fs.writeFile(path, string, { encoding: encoding });
async writeFile(path, string, encoding = 'base64') {
try {
return await fs.writeFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
// same as rm -rf
async remove(path) {
try {
return await fs.remove(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async move(source, dest) {
@@ -35,7 +64,7 @@ class FsDriverNode {
await time.sleep(1);
continue;
}
throw error;
throw this.fsErrorToJsError_(error);
}
}
@@ -77,12 +106,20 @@ class FsDriverNode {
return output;
}
open(path, mode) {
return fs.open(path, mode);
async open(path, mode) {
try {
return await fs.open(path, mode);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
close(handle) {
return fs.close(handle);
async close(handle) {
try {
return await fs.close(handle);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
readFile(path, encoding = 'utf8') {

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