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

Compare commits

..

481 Commits

Author SHA1 Message Date
Laurent Cozic
0c552feb57 Electron release v1.0.169 2019-09-27 19:02:09 +01:00
Laurent Cozic
cd1aa57243 Merge branch 'master' of github.com:laurent22/joplin 2019-09-27 19:01:51 +01:00
Laurent Cozic
c9098b0489 All: Improves deletion fail-safe so it is based on percentage of notes deleted. And display warning on sidebar. 2019-09-27 18:12:28 +00:00
Valentin Deville
117ce52597 Desktop: Add support for Deepin desktop environment in install script (#1884)
* Add support for Deepin desktop environment

* Update Joplin_install_and_update.sh
2019-09-26 21:53:11 +01:00
Laurent Cozic
1836b9a0b0 Merge branch 'master' of github.com:laurent22/joplin 2019-09-26 21:00:28 +01:00
FoxMaSk
563a4b7bd8 Repo: Drop emoji in tag name (#1899) 2019-09-26 20:46:42 +01:00
Laurent Cozic
2a5648d1a7 Tools: Fix build for Android 9 debugging 2019-09-26 19:26:18 +00:00
Laurent Cozic
5b425f9178 Android: Fixes "Row too big to fit into CursorWindow" error by allowing notes up to 50MB in size 2019-09-26 19:11:55 +00:00
Laurent Cozic
820e32ddca Doc: Removed default title in new issue templates
Because otherwise people leave them in, but they are redundant with
the automatically attached tag.
2019-09-26 15:21:00 +01:00
Laurent Cozic
b873e706ca Removed invalid files 2019-09-26 00:54:36 +01:00
Laurent Cozic
f0f6e7c856 Update website 2019-09-26 00:53:57 +01:00
Laurent Cozic
1ff3c7d074 Merge branch 'master' of github.com:laurent22/joplin 2019-09-26 00:53:34 +01:00
Laurent Cozic
393bb8993e Update readme 2019-09-25 22:42:59 +01:00
Helmut K. C. Tessarek
00ddb1eb64 update stale.yml 2019-09-25 17:41:39 -04:00
Laurent Cozic
cd518776a9 Update website 2019-09-25 22:28:48 +01:00
Laurent Cozic
391f7d22a3 CLI v1.0.147 2019-09-25 22:27:58 +01:00
Laurent Cozic
effbf10571 All: Log last requests in case of a sync error 2019-09-25 18:58:15 +00:00
Laurent Cozic
4405e94e0c Android release v1.0.306 2019-09-25 19:52:01 +01:00
Laurent Cozic
69f1b72127 Electron release v1.0.168 2019-09-25 18:54:50 +01:00
Laurent Cozic
383fa2e278 Update translations 2019-09-25 18:54:25 +01:00
Laurent Cozic
573100c203 Merge branch 'master' of github.com:laurent22/joplin 2019-09-25 18:46:52 +01:00
Laurent Cozic
348efdd7b6 All: Added fail-safe to prevent data from being wiped out when the sync target is empty 2019-09-25 18:40:04 +00:00
Laurent Cozic
cb9cc95e6a Doc: Fix email address 2019-09-25 16:29:21 +01:00
Laurent Cozic
1994b334fa Create SECURITY.md 2019-09-25 16:28:34 +01:00
Laurent Cozic
3e5a9cdb97 Tests: Make all tests use asyncTest for better error handling 2019-09-23 23:23:10 +01:00
Laurent Cozic
ae863c95c7 Tests: Make tests and CLI app less dependent on building the translations 2019-09-23 23:01:05 +01:00
Laurent Cozic
e89e5efb62 CI: Fix npm install 2019-09-23 22:45:07 +01:00
Laurent Cozic
9f2ce06829 CI: Build pull requests too 2019-09-23 22:41:22 +01:00
Laurent Cozic
ec89ebc6b0 Make build fail if tests don't pass 2019-09-23 22:37:14 +01:00
Laurent Cozic
f3e9668eb7 Tests: Fail test units when an uncaught exception is thrown inside asyncTest 2019-09-23 22:30:25 +01:00
Laurent Cozic
cc7e2fc456 Merge branch 'master' of github.com:laurent22/joplin 2019-09-23 22:27:16 +01:00
Devon Zuegel
172d925f0f Desktop: Fix import interop service (#1887)
* Revert "Revert "Desktop: Add ENEX to HTML export (#1795)""

This reverts commit 50b66cceca.

* Revert "Revert "Desktop, Cli: Fixed interop service so that it still allow auto-detecting importer based on format (required for Cli and for test units)""

This reverts commit c7c57ab2a5.

* Fix the .md importer

* Add comment re future refactor

* Rm importerClass for .md importer

* Fix EnexToMd module name
2019-09-23 22:18:30 +01:00
Laurent Cozic
8a8ecaade3 Update BUILD.md 2019-09-22 10:54:14 +01:00
Helmut K. C. Tessarek
d21a3f0bca update stale.yml 2019-09-21 14:55:26 -04:00
Helmut K. C. Tessarek
9322212601 update issue templates 2019-09-21 14:50:20 -04:00
Laurent Cozic
691eefec2f Desktop, CLI: Also allow importing TXT files with markdown 2019-09-20 23:00:59 +01:00
Laurent Cozic
50b66cceca Revert "Desktop: Add ENEX to HTML export (#1795)"
This reverts commit 2f14832c34.

Reverting PR #1795 due to broken MD import and other issues
2019-09-20 22:18:09 +01:00
Laurent Cozic
c7c57ab2a5 Revert "Desktop, Cli: Fixed interop service so that it still allow auto-detecting importer based on format (required for Cli and for test units)"
Reverting PR #1795 due to broken MD import and other issues

This reverts commit 558b6443bc.
2019-09-20 22:13:34 +01:00
Laurent Cozic
558b6443bc Desktop, Cli: Fixed interop service so that it still allow auto-detecting importer based on format (required for Cli and for test units) 2019-09-19 23:26:33 +01:00
Laurent Cozic
335b43ead4 Chore: Fixed a few formatting issues 2019-09-19 23:02:29 +01:00
Laurent Cozic
e648392330 Chore: Apply eslint rules 2019-09-19 22:51:18 +01:00
Laurent Cozic
ab29d7e872 Chore: Add line break style, and use of template to eslint rules 2019-09-19 22:49:17 +01:00
Woosuk Park
d7fae6b5b8 Update ko.po - korean language support (#1875) 2019-09-17 20:43:38 -04:00
Devon Zuegel
4ba4910a9c Mobile: Custom mobile editor font (#1797)
* Make editor font "Menlo"

* Add .vscode/* to .gitignore

* Add "editor font" config UI

* Render "editor font" chosen in config

* Add shim.mobilePlatform()

* Use style.editor.fontFamily rather than editorFont

* Add default font option

* Fixed for Android
2019-09-17 21:32:00 +01:00
Laurent Cozic
fe9a037cf9 Chore: Fixed package.json files and updated BUILD instructions 2019-09-17 21:29:37 +01:00
Devon Zuegel
2f14832c34 Desktop: Add ENEX to HTML export (#1795)
* Add `escape` to go back from Dropbox Login screen

* Add .vscode/ to .gitignore

* Remove call to enexXmlToMd

* The 2 enex importers have distinct functionality!

* Add tmp #deleteAllNotebooks

* checkbox state still not persisting

* images now fixed, but checkboxes still broken

* Figured out that #ipcProxySendToHost is important for handling checkbox

* cleanup closing br and en-todo tags + add notes

* Handle en-media, add NOTEs & TODOs, & format html

* Clean up some of the logging

* cleanHtml is a nice beautifier, but callback hell ensues...

* Rm #htmlFormat

* Recreating the xml actually seems to work

* Add test (not functional rn)

* Add test for checkboxes

* Add test for image en-media

* Separate tests into 2 function calls

* Clean up test

* Add `en-media-audio` test

* Add bad resource test

* Misc cleanup

* Rm SlateEditor files

* Misc cleanup

* Remove #deleteAllNotebooks button

* Add names to tests

* Extract resourceUtils

* Rm DropboxLoginScreen esc behavior, part of another PR

* Misc cleanup

* Improve audioElement, add attachment import support

* Misc cleanup

* Add svg test for enex_to_html

* Clean up test

* Set markup_language to MARKUP_LANGUAGE_HTML to tell renderer that the content is only HTML

* Rename to newModuleByFormat_ for clarity

* Add comment to clarify newModuleFromPath_
2019-09-17 21:19:32 +01:00
Laurent Cozic
52ace55db0 Mobile: Remove empty sections from config screen 2019-09-16 22:59:45 +01:00
Laurent Cozic
224a4d786b Desktop: Fixed broken menu bar 2019-09-16 22:54:40 +01:00
腹肌抽筋了
3ea97ad9ff Update zh_CN.po Chinese translation (#1871) 2019-09-15 00:02:53 -04:00
Laurent Cozic
e7a56bb2b1 ios-v10.0.37 2019-09-13 22:08:13 +01:00
Laurent Cozic
e4aed469d7 Chore: Add reslect to package 2019-09-13 22:05:02 +01:00
Laurent Cozic
15a42a3729 Chore: Apply eslint no-unused-vars eslint config and add TypeScript config 2019-09-12 22:16:42 +00:00
Laurent Cozic
0b9e007b46 Merge branch 'master' of github.com:laurent22/joplin 2019-09-12 22:48:49 +01:00
Laurent Cozic
88561a6c3c Desktop, CLI: Fixed import of notes that contain links with hashes 2019-09-12 22:48:10 +01:00
genneko
e6b77c3381 Update ja_JP.po Japanese translation (#1868)
* Update Japanese translation (Fix typos).

* Update Japanese translation (Improve the wording).

* Update Japanese translation (Add new items).
2019-09-12 17:38:48 -04:00
abonte
f7e1589476 Update it_IT.po (#1866) 2019-09-12 17:37:34 -04:00
Laurent Cozic
0379523eaf Desktop, Mobile: Fixes #1870: Support non-alphabetical characters in note link anchors 2019-09-12 21:57:23 +01:00
Laurent Cozic
5db7502fe4 Merge branch 'tidy_config' 2019-09-11 23:00:03 +01:00
Laurent Cozic
1578188fde Update website 2019-09-11 11:18:44 +01:00
Laurent Cozic
e8e1a0fe4d Desktop: Cleaned up and improved config screen design, move all screens under same one, and added section buttons 2019-09-10 23:53:01 +00:00
Laurent Cozic
8059009ff3 Android release v1.0.305 2019-09-10 09:31:48 +01:00
Laurent Cozic
32865f065c Electron release v1.0.167 2019-09-10 09:26:26 +01:00
Laurent Cozic
e03ef78049 All: Fixed link issue following last update 2019-09-10 09:25:58 +01:00
Laurent Cozic
45a820bb35 Update website 2019-09-09 19:03:22 +01:00
Laurent Cozic
3c26159b79 Doc: Add link to 32-bit Android APK to readme and website 2019-09-09 18:58:35 +01:00
Laurent Cozic
cf67e0d4af Tools: Allow specifying "to" commit option in git-changelog 2019-09-09 18:50:04 +01:00
Laurent Cozic
1b2767167d Cli: Upgrade joplin-turndown-plugin-gfm to fix import of certain Enex tables 2019-09-09 18:35:22 +01:00
Laurent Cozic
f07bb5c275 Android release v1.0.304 2019-09-09 18:26:55 +01:00
Laurent Cozic
0e2cc418e2 Electron release v1.0.166 2019-09-09 18:22:42 +01:00
Laurent Cozic
0340456d55 Update translations 2019-09-09 18:22:20 +01:00
Laurent Cozic
7aea2cec69 Desktop: Resolves #1490: Add support for anchor hashes in note links 2019-09-09 18:16:00 +01:00
Laurent Cozic
fa83107840 Doc: Update CLI installation info 2019-09-08 20:21:14 +01:00
Laurent Cozic
bb0bf46f81 CLI v1.0.146 2019-09-08 20:12:49 +01:00
Laurent Cozic
694c3fed2d Cli: Fixed regression that was making installation fail 2019-09-08 20:12:25 +01:00
Laurent Cozic
772e39b710 Tools: Improved git-changelog so that it is less error prone 2019-09-08 17:54:41 +01:00
Laurent Cozic
05e0a2c29d CLI v1.0.145 2019-09-08 17:24:00 +01:00
Laurent Cozic
78e0efb95f Trying CLI release 2019-09-08 17:19:22 +01:00
Laurent Cozic
a5f749cfd2 Tools: Added moment package 2019-09-08 17:18:44 +01:00
Laurent Cozic
3d6c932e1b Cli: Added headless server command (Beta) (#1860)
* Trying to implement headless server

* Cli: Cleaned up and completed server command so that it is usable. Added warnings as it is advanced usage only at this point.

* Restored welcome assets
2019-09-08 17:16:45 +01:00
archont00
4488a1b95f Doc: Update explanation of enabling E2EE (#1859)
As per https://discourse.joplinapp.org/t/totally-confused-re-e2ee/3295, I tried to improve the explanation of the process to avoid multiple encryption keys.
2019-09-08 16:35:10 +01:00
archont00
e2808a90c6 Doc: Update re. impossibility to delete unused Master Keys (#1858)
As per discussion at https://discourse.joplinapp.org/t/totally-confused-re-e2ee/3295/2, I propose to add info about Master Keys persistence.
2019-09-08 16:34:37 +01:00
Laurent Cozic
755a972e02 Update website 2019-09-08 11:09:02 +01:00
Laurent Cozic
8b1de22049 Merge branch 'master' of github.com:laurent22/joplin 2019-09-08 11:07:54 +01:00
Laurent Cozic
a9735123b7 Doc: Added warning to generated HTML files 2019-09-08 11:06:53 +01:00
Laurent Cozic
5ccafa2838 Doc: Added warning to generated HTML files 2019-09-08 10:46:35 +01:00
archont00
e2926a4f82 Doc: Update explanation of enabling E2EE (#1856)
As per https://discourse.joplinapp.org/t/totally-confused-re-e2ee/3295, I tried to improve the explanation of the process to avoid multiple encryption keys.
2019-09-08 10:35:15 +01:00
Laurent Cozic
09df315639 Desktop: Fixes #1833: Do not scroll text when search is open and user type in note 2019-09-07 11:57:31 +01:00
Laurent Cozic
5a9b3b6c7c Desktop, Mobile: Resolves #1832: Only support checkboxes that start with a dash 2019-09-07 11:18:07 +01:00
Laurent Cozic
6da6f35ddd Merge branch 'master' of github.com:laurent22/joplin 2019-09-07 10:47:51 +01:00
Laurent Cozic
dcb5590842 Clipper: Fixes #1851: Fixed error when trying to import certain pages using "Clip simplified page" feature 2019-09-07 10:47:31 +01:00
Laurent Cozic
5135c8a782 Clipper: Fixes #1783: Fixed importing tables that contain pipes 2019-09-07 10:32:52 +01:00
Laurent Cozic
1b2f4fb036 Update bug_report.md 2019-09-07 10:09:05 +01:00
Laurent Cozic
76a4a445f0 Doc: Added info about SKIP_PREFLIGHT_CHECK 2019-09-07 09:56:06 +01:00
Helmut K. C. Tessarek
20abb125a5 infrastructure: skip preflight check for building clipper 2019-09-06 13:53:35 -04:00
Laurent Cozic
be9e50b4a1 Update FUNDING.yml 2019-09-06 18:48:05 +01:00
Helmut K. C. Tessarek
02bfcf577d Clipper release v1.0.18 2019-09-06 13:37:55 -04:00
Laurent Cozic
038efa10f2 Update website 2019-09-06 18:34:47 +01:00
Laurent Cozic
dfa692569b Update translations 2019-09-06 18:33:30 +01:00
Laurent Cozic
9abc6a2e44 CLI: Fixes #1779: Make sure setting side-effects are applied even when running in command line mode 2019-09-06 18:29:40 +01:00
Laurent Cozic
11f23f4e00 Android release v1.0.303 2019-09-06 17:51:38 +01:00
Laurent Cozic
6a7d40d171 Merge branch 'master' of github.com:laurent22/joplin 2019-09-06 17:50:39 +01:00
Laurent Cozic
bf5601429e Mobile: Fixes #1767: Fixed broken search when certain notes are too large 2019-09-06 17:39:36 +01:00
abonte
73ae8aaf2f Update it_IT.po (#1853) 2019-09-05 22:45:36 -04:00
axq
7eb7bd98f3 Doc: Minor English improvements in README.md (#1852) 2019-09-05 17:11:45 +01:00
Laurent Cozic
10e22654ea Desktop: Fixes #1815: Fixed cropped content issue when printing or exporting to PDF 2019-09-04 20:11:35 +01:00
Joan Montané
ccfc80ad04 Update ca.po (#1836) 2019-08-29 17:39:35 +01:00
stellarpower
5e95278084 Fix Cinnamon Detection (#1828)
$desktop was converted to lowercase, but matched against 'X-Cinnamon'
2019-08-29 17:39:10 +01:00
Helmut K. C. Tessarek
d69ba6bc75 All: fix typo on encryption options screen (#1823)
fixes #1798
2019-08-29 17:38:54 +01:00
Caleb John
d28fbe2d3b Desktop: Apply current locale to date and time (#1822) 2019-08-29 17:38:24 +01:00
Caleb John
415e7b84da Desktop: Change localeCompare functiion for tags (#1811)
* change localeCompare functin for tags

* Fix spacing
2019-08-29 17:36:53 +01:00
Caleb John
ac4986b620 Desktop: Fixes #1803: Use correct date format for templates (#1810) 2019-08-29 17:35:43 +01:00
Caleb John
9a4f4cbb65 Desktop: Change template prompt to sans and sort templates (#1806)
* Change template prompt to sans and sort templates

* Sort templates by filename to ensure order
2019-08-29 17:34:54 +01:00
Helmut K. C. Tessarek
8e32957111 Infrastructure: build-translation.js - add support for macOS (#1804)
* remove unnecessary comment

It's totally fine to add the double quotes after -i.
Using gsed would mean that people had to install GNU sed. For what, if the same is possible with the system's sed with a slight modification.
Checking for gsed and using it is more trouble than it's worth.

* build-translation.js: add support for macOS

* implement requested changes
2019-08-29 17:34:05 +01:00
Helmut K. C. Tessarek
91aa3703d4 CLI: Fixes fatal error with cli 1.0.141 on start (#1791)
- updated terminal-kit to 1.30.0
- do not call method, if object does not exist

fixes #1778
2019-08-29 17:05:20 +01:00
FoxMaSk
a889762056 Update README.md
Double line fixed
2019-08-21 10:14:15 +02:00
Robert
6478d6c9c9 Update nl_NL.po (#1821) 2019-08-19 17:59:43 -04:00
FoxMaSk
7a681d0a4a README: add Discord Community Server (#1800)
* Discord Community Server

* Discord Server Link fixed

* improvment
2019-08-19 13:34:43 -04:00
Rafael Teixeira
83b6eba8bd Updated Last Translator pt_BR (#1813) 2019-08-17 02:36:25 -04:00
Helmut K. C. Tessarek
2766ded5f6 Update website 2019-08-16 19:55:27 -04:00
Rafael Teixeira
ca0d966ed9 Translation: Translation Update pt_BR (#1809)
* Updated translation pt-BR

* New updates translation pt_BR
2019-08-16 03:58:07 -04:00
Woosuk Park
386c583b0e Translation : Korean Translation #2 (#1802)
* Korean Language Support

Korean Language 100% Translated, but something is roughted. soon fix it ;)

* some improved

Some Improved

* Improve v2

some polished

* Ok, now Completed

For Now, Completed Polish(Change Respect of Users, and several fixing & polishing) + 100% Translated. Ready to Shipping

* Korean Language Update #2

1. 조플린 -> Joplin (Sorry i forgot)
2. Fuzzy correction.

Korean Language Update #2

1. 조플린 -> Joplin (Sorry i forgot)
2. Fuzzy correction.
2019-08-15 13:15:32 -04:00
Laurent Cozic
b3d34ad7e9 Android release v1.0.299 2019-08-14 23:47:38 +02:00
Laurent Cozic
ba5c636dda Android: Fixed AndroidX transition issue 2019-08-14 23:44:57 +02:00
Laurent Cozic
ea16f6e0b1 Revert "Trying to fix Android buikld"
This reverts commit 0dd0dc5489.
2019-08-14 23:23:27 +02:00
Laurent Cozic
0dd0dc5489 Trying to fix Android buikld 2019-08-14 23:22:52 +02:00
Laurent Cozic
f3ab21ff43 Electron release v1.0.165 2019-08-14 23:17:25 +02:00
Laurent Cozic
5ac6b46efd Desktop: Fixed theme options for Solarized theme 2019-08-14 23:17:02 +02:00
Laurent Cozic
6548f30a4b Electron release v1.0.164 2019-08-14 22:49:45 +02:00
Laurent Cozic
849d7983f6 Desktop: Added support for Fountain screenwriting language 2019-08-14 12:40:06 +02:00
Helmut K. C. Tessarek
e32e4423db change feature request template 2019-08-14 02:08:35 -04:00
Laurent Cozic
7f5bf131a8 Add back files that might have been modified by linter 2019-07-30 11:40:33 +02:00
Laurent Cozic
87a639df2b Mobile: Disabled solarized themes on mobile 2019-07-30 11:37:52 +02:00
Laurent Cozic
bdd8eab87e Mobile: Make it clearer when there are no notebooks and added a button create one 2019-07-30 11:36:56 +02:00
Laurent Cozic
b9e5c8a387 Electron release v1.0.163 2019-07-30 09:52:26 +02:00
Laurent Cozic
d646a2dd01 Fix Linux and macOS build to go around this bug: https://github.com/electron-userland/electron-builder/issues/3179 2019-07-30 09:52:16 +02:00
Laurent Cozic
71a3a0176e Android release v1.0.294 2019-07-30 09:42:09 +02:00
Laurent Cozic
a363d119cf Electron release v1.0.162 2019-07-30 09:37:23 +02:00
Laurent Cozic
ff08bdbc0b Merge branch 'master' of github.com:laurent22/joplin 2019-07-30 09:35:49 +02:00
Laurent Cozic
71efff6827 Linter update (#1777)
* Update eslint config

* Applied linter to lib

* Applied eslint config to CliClient/app

* Removed prettier due to https://github.com/prettier/prettier/pull/4765

* First pass on test units

* Applied linter config to test units

* Applied eslint config to clipper

* Applied to plugin dir

* Applied to root of ElectronClient

* Applied on RN root

* Applied on CLI root

* Applied on Clipper root

* Applied config to tools

* test hook

* test hook

* test hook

* Added pre-commit hook

* Applied rule no-trailing-spaces

* Make sure root packages are installed when installing sub-dir

* Added doc
2019-07-30 09:35:42 +02:00
Laurent Cozic
7595fe4a8c Merge branch 'master' of github.com:laurent22/joplin 2019-07-30 09:32:34 +02:00
Laurent Cozic
7697e75466 Update eslint config 2019-07-29 21:23:14 +02:00
Helmut K. C. Tessarek
b8fbaa2029 Update en_US.po 2019-07-29 11:34:52 -04:00
Helmut K. C. Tessarek
38bc750ecf Update de_DE.po 2019-07-29 11:34:02 -04:00
Laurent Cozic
6cfacb1a48 Second pass at linting lib dir 2019-07-29 15:58:33 +02:00
Laurent Cozic
0b9078d034 config 2019-07-29 15:58:17 +02:00
Laurent Cozic
86dc72b204 First pass at linting lib dir 2019-07-29 15:43:53 +02:00
Laurent Cozic
64b7bc3d62 More config 2019-07-29 15:43:39 +02:00
Laurent Cozic
086f9e1123 Started applying config to Electron app 2019-07-29 14:13:23 +02:00
Laurent Cozic
4fe70fe8ee Ignore some files 2019-07-29 14:13:09 +02:00
Laurent Cozic
95a1f40404 Tweaked linter config 2019-07-29 14:10:07 +02:00
Laurent Cozic
88f04509ee Also added prettier config 2019-07-29 13:48:43 +02:00
Laurent Cozic
7eebd544d6 Fixed eslint 2019-07-29 12:55:50 +02:00
Laurent Cozic
e369a8decf Added eslint config 2019-07-29 12:55:39 +02:00
Helmut K. C. Tessarek
ad8054ba4b Desktop: Better handling of adding the title to print and export to PDF (#1744)
fixes #1743
2019-07-29 12:33:40 +02:00
J0J0 T
b47cb4e29a Desktop, Cli: Improved bold formatting support in Enex import (#1708)
* Dekstop,CLI: enex_to_md: add html/md test file pairs

* one pair for basic text formatting tags: strong, b, i, em
* and one using span tags with inline styles for bold formatting

Note: The html files include the Evernote-typical "linebreak tags inside of separate <div> tags"
to represent empty lines!

* Desktop,Cli: enex_to_md: support bold in span tags using inline styles

* function isSpanWithStyle() checks if further processing of a span tag
  makes sense
* function isSpanStyleBold() checks if bold formatting via styles is
  used - a similar function could be written for each span-inline-style-format
  that should be supported

* Desktop,Cli: enex_to_md: fix saving span attrs in state object

pushing attributes of span tag to state object now
happens outside of isSpanWithStyle()
2019-07-29 12:25:25 +02:00
Laurent Cozic
8c42ddf6c3 Merge branch 'master' of github.com:laurent22/joplin 2019-07-29 12:17:33 +02:00
tfinnberg
f7fcabbf41 Desktop: Create fileURLs via drag and drop (#1653)
* enable drag and drop fileURLs

* fix windows fileURL syntax

* introduce encodeURI function

* fixed encoding issue

* use path-utils.js to deal with fileURL path conversion

* add changes as requested

* Minor rewording 'On the' -> 'In the', additional info about attaching files

* change call of toFileProtocolPath

* enable test script to check syntax for all OS-platforms
2019-07-29 12:16:47 +02:00
Shane Kilkelly
38a51070fc Desktop: add depthColor for solarized light and dark themes (#1765) 2019-07-29 12:08:49 +02:00
Laurent Cozic
44fa099a77 Desktop: No longer crash if certain theme properties are not set 2019-07-29 12:05:58 +02:00
sumomo-99
af6f3999df Update ja_JP.po (#1776) 2019-07-29 11:55:02 +02:00
Laurent Cozic
6fbeb35951 Update translations 2019-07-29 11:52:19 +02:00
Laurent Cozic
b2eadffde0 Using British English by default for consistency 2019-07-29 11:51:09 +02:00
Laurent Cozic
200ba2775f All: Resolves #1459: Make translation files smaller by not including untranslated strings. Also add percentage translated to config screen. 2019-07-29 11:47:50 +02:00
Laurent Cozic
39ba021a79 Merge branch 'master' of github.com:laurent22/joplin 2019-07-29 10:12:45 +02:00
Laurent Cozic
2c6b291b9b Desktop: Only repeat failed requests up to 3 times during sync 2019-07-29 10:12:23 +02:00
Devon Zuegel
770846be2e Doc: Add docs to clarify how to test (#1775) 2019-07-29 09:42:10 +02:00
Laurent Cozic
af4aa01b75 Mobile: Upgraded packages to fix security issues 2019-07-28 23:03:54 +02:00
Laurent Cozic
1bf2bec805 CLI v1.0.141 2019-07-28 22:54:45 +02:00
Laurent Cozic
3a41ac9be0 CLI: Upgraded packages to fix security issues 2019-07-28 22:54:29 +02:00
Laurent Cozic
f2c9cdd7f1 Desktop: Upgraded packages to fix security issue 2019-07-28 22:48:30 +02:00
Laurent Cozic
058f418cc7 Tools: Do not display full release info when releasing Android app 2019-07-28 22:43:33 +02:00
Laurent Cozic
5fa84b0dfb Android release v1.0.293 2019-07-28 18:36:36 +02:00
Laurent Cozic
ec8ec3e38d Android release v1.0.292 2019-07-28 18:34:44 +02:00
Laurent Cozic
0e6190b42b Tools: Allow creating multiple Android releases 2019-07-28 18:33:48 +02:00
Laurent Cozic
fcfee36c8c Android release v1.0.291 2019-07-28 18:32:33 +02:00
Laurent Cozic
1d3d3b99bb Tools: Allow creating multiple Android releases 2019-07-28 18:31:31 +02:00
Laurent Cozic
2f80bf9647 Android release v1.0.290 2019-07-28 18:29:07 +02:00
Laurent Cozic
675a4c795f Android release v1.0.289 2019-07-28 18:22:33 +02:00
Laurent Cozic
ed3361df57 Merge branch 'master' of github.com:laurent22/joplin 2019-07-28 18:22:33 +02:00
Laurent Cozic
87396572e4 Tools: Allow creating multiple Android releases 2019-07-28 18:19:41 +02:00
Woosuk Park
143b610291 Korean Language Support (#1769)
* Korean Language Support

Korean Language 100% Translated, but something is roughted. soon fix it ;)

* some improved

Some Improved

* Improve v2

some polished

* Ok, now Completed

For Now, Completed Polish(Change Respect of Users, and several fixing & polishing) + 100% Translated. Ready to Shipping
2019-07-26 23:18:47 -04:00
Laurent Cozic
c952c4591f Android release v1.0.284 2019-07-26 23:11:08 +02:00
Laurent Cozic
e418701e68 Mobile: Fixes #1764 (maybe): Upgrading React-Native to 0.59.10 to try to fix crash in certain Samsung phone due to 64-bit version
See also:

https://github.com/facebook/react-native/issues/24261#issuecomment-514332549
2019-07-26 23:08:11 +02:00
Laurent Cozic
6fc0ee3062 Android release v1.0.283 2019-07-26 21:26:24 +02:00
Laurent Cozic
7b42d7d2c8 Mobile: Fixed freeze when last notebook was the conflict one and there are no longer any conflicted notes 2019-07-26 21:23:18 +02:00
Mr. Traduttore
eb083ae925 [Mobile/Desktop] Changed cancel, directory and delete translations (ITALIAN) (#1759)
* Changed cancel and delete translation (ITALIAN)

Changed cancel from "cancellare" to "annullare" (whic is used more to express the cancelling operations) and delete from "cancellare" to "eliminare". I think that doing this will help people understanding what is the main task of these option since they use the same verbs in the current translation.

* Update it_IT.po
2019-07-22 14:15:11 +01:00
Helmut K. C. Tessarek
905e65365f Update en_US.po 2019-07-22 06:28:04 -04:00
Helmut K. C. Tessarek
a0c04c0e6a Update de_DE.po 2019-07-22 06:27:55 -04:00
Laurent Cozic
635baa5b6f Updated translations 2019-07-22 00:02:25 +00:00
Laurent Cozic
7591b614c5 typos 2019-07-21 22:27:13 +00:00
Helmut K. C. Tessarek
5b5ec682c0 Update en_US.po 2019-07-21 22:18:35 -04:00
Helmut K. C. Tessarek
b0a80ddf65 Update de_DE.po 2019-07-21 22:17:25 -04:00
Laurent Cozic
893531f8c7 Desktop: Make depthColor theme property optional 2019-07-21 17:30:36 +01:00
Shane Kilkelly
3d498e7a75 Desktop: Add solarized themes to desktop client (#1733) 2019-07-21 17:27:42 +01:00
Laurent Cozic
45ad201132 Mobile: Fixed colour of placeholder text in dark theme 2019-07-21 17:16:15 +01:00
Laurent Cozic
6e64b950ca Merge branch 'master' of github.com:laurent22/joplin 2019-07-21 17:05:09 +01:00
Laurent Cozic
6d4e67769c Desktop: Fixed Back button icon on Config screen 2019-07-21 16:49:53 +01:00
Laurent Cozic
3aa0394062 Mobile: Fixes #1684: Trying to fix problem when attaching file that contains spaces in name 2019-07-21 14:39:52 +01:00
Laurent Cozic
b65767a43c Mobile: Fixes #1746: Populate note side menu when swiping right 2019-07-21 14:11:30 +01:00
Laurent Cozic
3a9817d11e Android release v1.0.282 2019-07-21 14:04:18 +01:00
Laurent Cozic
6a42ef50ec All: Fixes #1732: Fixed note order when dragging a note outside a notebook 2019-07-21 13:55:25 +01:00
Laurent Cozic
35b6b3fc46 Merge branch 'clipper_html_mode' 2019-07-21 13:46:54 +01:00
Laurent Cozic
f5515e3496 Doc: Fixed template doc 2019-07-21 09:07:33 +01:00
Laurent Cozic
fd509bb4af Doc: Fixed template table 2019-07-21 08:57:50 +01:00
Laurent Cozic
b21c0f5d69 Doc: Fixed website templating 2019-07-21 08:55:44 +01:00
Laurent Cozic
1033b3626f Update translations 2019-07-21 08:49:49 +01:00
Laurent Cozic
f407c8d756 Added Serbian translation - thanks Željko! 2019-07-21 08:19:32 +01:00
Laurent Cozic
6436dff94b Api: Fixed handling of markup language 2019-07-21 00:31:29 +01:00
Laurent Cozic
3f7b4e10b6 Merge branch 'master' into clipper_html_mode 2019-07-21 00:24:25 +01:00
Laurent Cozic
36168a9a5d Use regex instead of jsdom for compability with mobile app 2019-07-21 00:18:51 +01:00
Helmut K. C. Tessarek
118540c733 add config for stale bot 2019-07-20 17:41:49 -04:00
Caleb John
cd5d412c69 Desktop: Added support for templates (#1647)
* First pass of adding support for templates

* remove default value from template prompt

* Add template placeholder text

* Add mustache templates with datetime support for new notes

* Moved template code to utils, added separate prompt for templates

* Add templates to menu and allow for keyboad only use

* update template prompt for dark theme

* update with laurents suggestions, add refresh button

* revert template command, remove new note prompt
2019-07-20 22:13:10 +01:00
Helmut K. C. Tessarek
e29fb3eb66 new issue templates 2019-07-19 15:01:32 -04:00
Laurent Cozic
8ff1668c8f Minor changes 2019-07-19 18:18:05 +01:00
Laurent Cozic
14fc73b388 Use markupToHtml everywhere 2019-07-19 17:48:38 +01:00
Laurent Cozic
f34330f101 Doc: Contributor list on rows of 5 2019-07-18 20:46:01 +01:00
Laurent Cozic
2ba50321d4 Doc: Added contributors 2019-07-18 18:36:29 +01:00
Laurent Cozic
38177c7e54 All: Optimised loading of multiple items 2019-07-17 22:50:12 +01:00
Laurent Cozic
687e308a73 Desktop: Added markup language to property dialog 2019-07-17 22:49:40 +01:00
Laurent Cozic
490db0db62 Desktop: Disable Markdown actions for HTML notes 2019-07-17 22:49:12 +01:00
Laurent Cozic
feb5f17479 Clipper: Generate better HTML so that it loads faster in text editor 2019-07-17 22:48:13 +01:00
Laurent Cozic
fbb3543818 Desktop: Fixed race condition when loading a note while another one is still loading. Improved performance when loading large note. 2019-07-17 22:42:53 +01:00
Laurent Cozic
30d0dfb424 Clipper: Fixed sizing issue when importing HTML pages 2019-07-16 22:58:44 +01:00
Laurent Cozic
7239a2013c Clipper: Improved clipping of images in HTML mode 2019-07-16 22:23:04 +01:00
Laurent Cozic
2361c5a5e7 Clipper: Better handling of images when multiple images have the same source but with different dimensions 2019-07-16 21:47:44 +01:00
Laurent Cozic
38e8a881d5 More refactoring to easily handle multiple renderers 2019-07-16 19:05:47 +01:00
Helmut K. C. Tessarek
d45d1b4225 Mobile: Show correct time/date (as specified in settings) for note properties (#1749) 2019-07-16 15:11:58 +01:00
Helmut K. C. Tessarek
e9c88dfdc4 Update de_DE.po 2019-07-15 23:21:46 -04:00
Laurent Cozic
fbb0ac5892 Clipper: Refactored image rules to re-use more code 2019-07-15 20:43:28 +00:00
Helmut K. C. Tessarek
7a902bbd25 Desktop, Mobile: Update Markdown plugins: footnote, toc-done-right, anchor (#1741) 2019-07-15 01:29:44 +01:00
Laurent Cozic
c75618eb8f Clipper: Minor fixes 2019-07-15 01:17:17 +01:00
Laurent Cozic
74ee629266 Clipper: Fixed issue with relative links when importing HTML 2019-07-15 00:44:45 +01:00
Caleb John
8ecc58e1bf Desktop: Add support for cinnamon to install script (#1738) 2019-07-15 00:29:09 +01:00
Laurent Cozic
71078637db Android release v1.0.281 2019-07-14 19:56:46 +01:00
Laurent Cozic
5460a977b1 Clipper: Fixed issues related to viewing images and to HTML comments that were incorrectly being displayed 2019-07-14 19:52:57 +01:00
Laurent Cozic
a0dd0702fb Clipper: Adding support for clipping page as HTML 2019-07-14 16:00:02 +01:00
Laurent Cozic
3e48992eb4 Clipper: Disabled preview for now as too many problems with it and not so useful 2019-07-14 09:46:06 +01:00
Laurent Cozic
bdb31f2890 Mobile: Fixed issue that could slow down app when displaying large list of notes 2019-07-13 23:55:28 +01:00
Laurent Cozic
0255546ae1 Mobile: Auto-save after toggling to-do state 2019-07-13 23:49:35 +01:00
Laurent Cozic
d066350eea Electron release v1.0.161 2019-07-13 19:18:34 +01:00
Laurent Cozic
a1e3260309 Clipper: Resolves #1160: Allow importing MathJax formulas, in particular from StackExchange 2019-07-13 19:17:28 +01:00
Laurent Cozic
be1f57a8a6 Desktop: Fixes #1727: Keep back button when opening a note link from the search results 2019-07-13 17:58:59 +01:00
Laurent Cozic
ca4dfe0f0f Desktop: Fixes #1724: Improved note selection and scrolling when moving a note to a different notebook 2019-07-13 17:40:09 +01:00
Laurent Cozic
fa69957d3f Desktop, CLI: Fixes #1723: Import Evernote audio files correctly 2019-07-13 17:26:47 +01:00
Laurent Cozic
f7203ed7e2 Desktop: Fixes #1720: Fixed issue with certain commands being repeated in some cases 2019-07-13 16:57:33 +01:00
Laurent Cozic
331858bd4f Desktop: Fixes #1704: Set note title to correct size when zoom is enabled 2019-07-13 16:46:52 +01:00
Laurent Cozic
4d2c9523a3 Desktop: Fixes #1699: Hide toolbar button text when it is below a certain size 2019-07-13 16:42:57 +01:00
Laurent Cozic
4d9d84a8f3 Merge branch 'master' of github.com:laurent22/joplin 2019-07-13 15:58:06 +01:00
Laurent Cozic
ec1089870f All: Fixes #1694: When deleting resource from sync target also delete associated data blob 2019-07-13 15:57:53 +01:00
Helmut K. C. Tessarek
dbedefc021 Desktop: Fixes #1342: Add override for ACE editor shortcut Ctrl+K (#1705) 2019-07-13 14:52:32 +01:00
Kirill Goncharov
f6b0da3f5e Android: Check filesystem permission if filesystem sync target is selected (#1665)
* Android: Check filesystem permission if filesystem sync target is selected

* Android: Change permission error text, don't prevent user from saving settings
2019-07-13 14:51:54 +01:00
Laurent Cozic
85bf89fd97 Android release v1.0.279 2019-07-12 19:44:23 +01:00
Laurent Cozic
c2a80b12f0 Mobile: More rendering optimisations to make animations smoother and to allow typing fast on large notes 2019-07-12 19:36:12 +01:00
Laurent Cozic
981c97cca5 Mobile: Optimising screen rendering to make text input faster 2019-07-12 18:32:08 +01:00
Laurent Cozic
091cbc5355 Mobile: Added sync button animation; Added notebook header; Improved layout of Edit Notebook screen 2019-07-12 18:07:47 +01:00
Laurent Cozic
e5a8114887 Mobile: Added icons to left sidebar 2019-07-11 18:44:26 +01:00
Laurent Cozic
4779fc6f43 Mobile: Remove search and side menu button from config screen 2019-07-11 18:26:04 +01:00
Laurent Cozic
86e7daaec4 Mobile: Cleaned context menu and moved options and metadata to side menu bar 2019-07-11 18:23:29 +01:00
Laurent Cozic
cab73a26e7 Mobile: Adding note side menu 2019-07-11 17:43:55 +01:00
Laurent Cozic
554ddb3b51 Mobile: Grouped file attachment action under one menu 2019-07-11 17:41:13 +01:00
Laurent Cozic
3b22bdb8ae Doc: Fixed Linux install command line for website 2019-07-10 18:18:31 +01:00
Laurent Cozic
5fdd07679e All: Fix: Only log master key ID 2019-07-10 17:35:08 +01:00
Laurent Cozic
69f75a1520 Doc: Updated old links 2019-07-10 17:00:10 +01:00
Laurent Cozic
f9b7acb8b1 Merge branch 'master' of github.com:laurent22/joplin 2019-07-10 16:41:20 +01:00
Laurent Cozic
91f700ad54 Doc: Updated email addresses 2019-07-10 16:41:13 +01:00
Robert
966aca7753 Update nl_NL.po (#1709)
* Update nl_NL.po

* Added ampersands to main menu items 

for the nl-nl translation
2019-07-05 13:00:53 -04:00
Helmut K. C. Tessarek
4de8816ed5 Merge pull request #1710 from Marmo/Marmo-translation-20190705
Translation: minor fixes (de_DE)
2019-07-05 11:26:32 -04:00
Marmo
bea68a1056 Minor translation fixes (de_DE) 2019-07-05 13:54:11 +02:00
Laurent Cozic
6fea7116b6 Mobile: Removed now unneeded Welcome screen 2019-06-29 00:24:00 +01:00
Laurent Cozic
2955914ca5 Mobile: Fixes #1690 (maybe): Process less data simultaneously when building search index to prevent out of memory errors 2019-06-28 23:49:43 +01:00
Laurent Cozic
fd150b5b9d Update website 2019-06-28 16:08:50 +01:00
Laurent Cozic
334ffad196 Doc: Added link to Mastodon feed 2019-06-28 16:04:05 +01:00
Charles
a796a9d179 API: Support is_todo property to allow making a note a todo (#1688) 2019-06-28 13:46:55 +01:00
Helmut K. C. Tessarek
917dcea28a use a command to get the current branch that works with older git versions
closes #1695
2019-06-28 05:02:21 -04:00
Laurent Cozic
c901228dc5 Merge branch 'master' of github.com:laurent22/joplin 2019-06-28 00:51:14 +01:00
Laurent Cozic
da21580785 Mobile: Added 'All notes' screen; Cleaned up header bar buttons; Removed 'body' from note preview object to improve memory usage 2019-06-28 00:51:02 +01:00
Laurent Cozic
4d92187327 Mobile: Added button to fix search engine index 2019-06-28 00:48:52 +01:00
Helmut K. C. Tessarek
207d433fb3 Desktop: Improved: Show git branch and hash in About dialog (#1692)
* show git branch and hash in About dialog

This additional info will only be shown, if the code is not an official release.

An official release is calculated as follows:
- current commit has a tag
- the tag contains the version number of the package

However, the information will always be printed to the console.

* info will now always be shown in About dialog (if available)

ElectronClient/app/compile-package-info.js: added warning
ElectronClient/app/app.js: push info conditionally to the message array

* use sprintf syntax
2019-06-27 15:04:02 +01:00
Laurent Cozic
ffc311d7bd Mobile: Moved slider value to the left so that it is visible while dragging 2019-06-27 00:05:17 +01:00
Laurent Cozic
a1e8e71359 Mobile: Added auto-save 2019-06-26 23:26:26 +01:00
Laurent Cozic
7942e74dc6 Mobile: Fixed field focus logic 2019-06-26 23:21:12 +01:00
Laurent Cozic
c4e21c2b6a Mobile: Added placeholders for note title and body, and focus body by default for notes 2019-06-26 23:00:25 +01:00
Laurent Cozic
0a06aa6f9f Mobile: When inside a note, do not show side menu and search buttons 2019-06-26 22:54:09 +01:00
Laurent Cozic
f985cfa25c Mobile: Removed arrow icon as it is rendered weirdly on device 2019-06-26 18:51:12 +01:00
Laurent Cozic
6e143aef5c Android release v1.0.277 2019-06-26 18:40:43 +01:00
Laurent Cozic
bf16aa6192 All: Better logging in case of error while indexing search 2019-06-26 18:36:42 +01:00
Laurent Cozic
d96c58d192 Mobile: Edit and delete notebooks by long-pressing them, and removed context menu on note lists 2019-06-26 18:28:09 +01:00
Laurent Cozic
e7e0264411 Mobile: Improved side menu: Made button panel fixed at the bottom, and added dark overlay over right side content 2019-06-26 18:05:37 +00:00
Laurent Cozic
430a11282b Mobile: Moved 'New notebook' button to sidebar 2019-06-26 01:10:15 +01:00
Laurent Cozic
9957b2798c Mobile: Moved config menu item to button on side bar 2019-06-26 00:35:26 +01:00
Laurent Cozic
2c5b0010bf Mobile: Removed concept of Advanced Options and move tools to Config screen to clean up context menu 2019-06-26 00:13:13 +01:00
Laurent Cozic
1e3c6ed98c Desktop: When doing local search do not split query into words 2019-06-25 23:09:53 +01:00
Laurent Cozic
484f290eb0 Clipper: Improved clipping selection by removing unecessary elements 2019-06-25 22:11:12 +01:00
Helmut K. C. Tessarek
06ad539941 Clipper release v1.0.17 2019-06-23 23:23:07 -04:00
Laurent Cozic
5b84e80ac4 Clipper: Fixes #1214: Include data from form fields when clipping forms 2019-06-24 00:57:39 +01:00
Laurent Cozic
ca0f349348 Merge branch 'master' of github.com:laurent22/joplin 2019-06-24 00:00:24 +01:00
Laurent Cozic
d79089aea3 Clipper: Fixes #1682: Do not clip elements that should be hidden 2019-06-24 00:00:11 +01:00
Eugene Odeluga
03611ad5ca Desktop: For Ubuntu users, added unity to if condition for desktop icon creation (#1683) 2019-06-23 22:24:58 +01:00
Helmut K. C. Tessarek
c78c1cd3cf Clipper release v1.0.16 2019-06-23 03:06:34 -04:00
水货
55afa7b5b7 Update zh_CN.po (#1681) 2019-06-23 03:00:41 -04:00
Laurent Cozic
a6c407b62b Doc: Mentioned Goto Anything feature 2019-06-22 19:06:54 +01:00
Laurent Cozic
21897a3cd4 Clipper: Resolves #1669: Handle special case of code block used on Microsoft website 2019-06-22 18:57:41 +01:00
Laurent Cozic
5796dd2098 Update translations 2019-06-22 13:10:13 +01:00
abonte
d050071437 update it_IT.po (#1680) 2019-06-22 12:45:35 +01:00
Laurent Cozic
eaf8510f49 Doc: Added requirement for unit tests 2019-06-22 12:44:31 +01:00
Laurent Cozic
6ee2595dce Desktop: Fixes #1676: Preserve user timestamps when adding note via API 2019-06-22 12:31:04 +01:00
Laurent Cozic
0ecf2d6d9a Merge branch 'master' of github.com:laurent22/joplin 2019-06-22 11:23:35 +01:00
Laurent Cozic
50fd075168 Desktop, CLI: Fixes #1672: Fix line break issue when importing certain notes from Evernotes 2019-06-22 11:23:22 +01:00
Helmut K. C. Tessarek
6fa76bb83a fix minor typo in README.md 2019-06-21 23:58:44 -04:00
Laurent Cozic
b175c1fc94 Desktop: Resolves #1649: Cache code blocks in notes to speed up rendering 2019-06-21 08:28:59 +01:00
Caleb John
b461625518 Desktop: Fixed issue with issue with watching file on Linux (#1659)
Watch for rename events in the external editor and re-watch file
2019-06-20 00:44:51 +01:00
Laurent Cozic
3819897ba1 Merge branch 'master' of github.com:laurent22/joplin 2019-06-20 00:02:29 +01:00
Laurent Cozic
6a031857ba Desktop: Fixes #1664: Disable certain menu items when no note or multiple notes are selected, and fixed menu item to set tag 2019-06-20 00:02:13 +01:00
Laurent Cozic
0e57b7eb46 iOS v10.0.36 2019-06-19 23:22:35 +01:00
Laurent Cozic
e21a0ba5b7 Update translations 2019-06-19 23:18:24 +01:00
Laurent Cozic
78f731e616 Merge branch 'master' of github.com:laurent22/joplin 2019-06-19 23:16:45 +01:00
Laurent Cozic
f6688a65ae iOS: Fixed bug that was preventing images from displaying 2019-06-19 23:16:37 +01:00
Laurent Cozic
035b9c6d1a Android: This is now needed to build the app 2019-06-19 21:51:22 +01:00
Laurent Cozic
266ff244d9 Revert "Mobile: Added button to clear local sync state"
Can cause too many issues.

This reverts commit 6ce091f4d8.
2019-06-19 21:50:26 +01:00
Laurent Cozic
de1bfa5c34 Android release v1.0.276 2019-06-19 15:08:44 +01:00
Laurent Cozic
478b8f00d8 Fix release script 2019-06-19 15:04:46 +01:00
Laurent Cozic
860d2fd7f5 Merge branch 'master' of github.com:laurent22/joplin 2019-06-19 15:04:16 +01:00
Laurent Cozic
6ce091f4d8 Mobile: Added button to clear local sync state 2019-06-19 14:57:59 +01:00
Laurent Cozic
ce595ac5e4 Mentioned that it works on FreeBSD 2019-06-19 12:29:15 +01:00
Laurent Cozic
267436a00d Merge pull request #1660 from delta-emil/bulgarian-translation
Localization: add bulgarian
2019-06-18 22:54:52 +01:00
Helmut K. C. Tessarek
97e0f4258a Update localization en_US.po 2019-06-18 12:32:56 -04:00
delta-emil
faa6ccc150 add bulgarian translation 2019-06-16 23:39:20 +03:00
Laurent Cozic
349cade946 All: Optimised resource download queue by exiting early if resources are already downloaded 2019-06-15 21:48:37 +01:00
Laurent Cozic
0200aa92de Doc: Added doc for resource download mode 2019-06-15 21:28:31 +01:00
Laurent Cozic
60ed2cbee5 Mobile: Fixed bug where photo was not displayed just after having taken it 2019-06-15 21:23:30 +01:00
Laurent Cozic
0e7b2f36c8 Fixing release process for Android 2019-06-15 18:58:09 +01:00
Laurent Cozic
4083221b21 Android: Added support for 64-bit hardware 2019-06-15 18:48:07 +01:00
Laurent Cozic
d55c511b4a iOS v10.0.35 2019-06-15 18:18:17 +01:00
Laurent Cozic
7863e1dffe Update website 2019-06-15 10:27:44 +01:00
Laurent Cozic
7cfdf778de Android release v1.0.271 2019-06-15 09:44:34 +01:00
Laurent Cozic
075e55c077 Android release v1.0.269 2019-06-15 01:23:12 +01:00
Laurent Cozic
dc818e8a0c Electron release v1.0.160 2019-06-15 00:57:36 +01:00
Laurent Cozic
caa58dd913 name 2019-06-15 00:57:13 +01:00
Laurent Cozic
c84c3cd026 Merge pull request #1655 from laurent22/react-native-5-9
React Native 5 9
2019-06-14 23:32:12 +01:00
Laurent Cozic
bda5ac9fb5 Upgraded RNdocument-picker 2019-06-14 23:23:01 +01:00
Laurent Cozic
f928f645e5 Upgraded RNSqlite to remove warnings 2019-06-14 23:16:37 +01:00
Laurent Cozic
53d7e906d4 Removed unneeded class that was causing a require cycle 2019-06-14 23:04:05 +01:00
Laurent Cozic
e670b5d03f Upgraded RNDialogbox to remove warnings 2019-06-14 23:02:35 +01:00
Laurent Cozic
2a7d555859 Replaced deprecated Slider component 2019-06-14 22:59:27 +01:00
Laurent Cozic
e6675f500c Removed no longer used WebView module 2019-06-14 22:46:08 +01:00
Laurent Cozic
c3f20d3ebc Getting latest RNCamera to work with Android 2019-06-14 22:45:35 +01:00
Laurent Cozic
ff257060d1 Trying get RNCamera to work in iOS 2019-06-14 22:31:01 +01:00
Laurent Cozic
68cde202a4 Added JSC to improve performances of Android app 2019-06-14 18:37:46 +01:00
Laurent Cozic
1a7a87e170 Remove this for now as are not on AndroidX yet 2019-06-14 18:30:16 +01:00
Laurent Cozic
2990642923 Trying to fix rn-camera but cannot be full fixed yet due to https://github.com/react-native-community/react-native-camera/pull/2306 2019-06-14 09:15:38 +01:00
Laurent Cozic
0818de036e Fixing issue with webview so that WebKit rendering can be used 2019-06-14 09:14:01 +01:00
Laurent Cozic
122bc29035 Mobile: Upgraded WebView 2019-06-14 08:11:15 +01:00
Laurent Cozic
861cf8a1b2 Merge branch 'master' into react-native-5-9 2019-06-14 07:22:57 +01:00
Laurent Cozic
7d6959e9e4 Merge branch 'master' of github.com:laurent22/joplin 2019-06-14 07:22:38 +01:00
Laurent Cozic
85091052e7 iOS: Fixed missing Confirm and Cancel buttons on Alarm dialog 2019-06-14 07:22:25 +01:00
Laurent Cozic
f46ad5bfda Upgrading to React Native 5.9 2019-06-14 07:12:24 +01:00
Laurent Cozic
29f7937fc2 Merge branch 'master' of github.com:laurent22/joplin 2019-06-13 08:48:30 +01:00
Laurent Cozic
7fae9fda10 Desktop: Fixes #1443: Allow opening external editor on new notes 2019-06-13 08:48:19 +01:00
Laurent Cozic
a37961dccc Desktop: Removed placeholder in tag list because it repeats the label 2019-06-13 08:47:47 +01:00
Helmut K. C. Tessarek
55155646aa Clipper release v1.0.15 2019-06-12 19:38:32 -04:00
Laurent Cozic
7bffe86439 Merge branch 'master' of github.com:laurent22/joplin 2019-06-13 00:26:55 +01:00
Laurent Cozic
86136e0c6c Clipper: Fixes #1622: Import named anchors from clipped pages 2019-06-13 00:26:19 +01:00
Helmut K. C. Tessarek
21ae447d9c Clipper: Create a zip file of the source (for validation by Firefox reviewers) (#1648)
The file `joplin-webclipper-source.zip` will be in the `dist` directory of the webclipper.
2019-06-12 23:11:24 +01:00
Laurent Cozic
ad211b4b4e Clipper: Fixes #1600: Handle SVG images and fix issue with invalid file extensions 2019-06-12 09:45:31 +01:00
Laurent Cozic
d6218f35fe Clipper: Fixes #1526: Local files can be clipped again 2019-06-11 01:09:48 +01:00
Laurent Cozic
7af0dcd19a Merge branch 'master' of github.com:laurent22/joplin 2019-06-11 00:11:57 +01:00
Laurent Cozic
e1a52c5606 CLI: Remove Welcome notes because they are mostly relevent to desktop and mobile, and there is already intro text on CLI 2019-06-11 00:11:49 +01:00
Helmut K. C. Tessarek
468c345527 Desktop: Improved: Added shortuct for tags (Cmd+Opt+T / Ctrl+Alt+T) (#1638)
fixes #1626
2019-06-10 23:45:49 +01:00
Caleb John
041bdc08a2 Desktop: New: Highlight notebooks based on depth (#1634)
* Highlight notebooks based on depth

* Adjusted notebook depth targets, and dark theme select color
2019-06-10 23:44:51 +01:00
Laurent Cozic
7535f1a8c6 Update website 2019-06-10 23:41:32 +01:00
Laurent Cozic
0b24433db3 CLI v1.0.140 2019-06-10 08:56:11 +01:00
Laurent Cozic
f7de0c5ffd Merge branch 'master' of github.com:laurent22/joplin 2019-06-10 08:55:46 +01:00
Laurent Cozic
62c48b9a46 CLI: Fixed regression which was preventing decryption on newly created profiles 2019-06-10 08:55:36 +01:00
Helmut K. C. Tessarek
3fafda9684 Desktop: Added menu item to format inline code (#1641)
* Revert "Fix for #1426, aded backticks to auto-wrapping quotes."

This reverts commit 7dee93076a.

* changes to backtick behavior

- add shortcut ``` cmd+` ``` / ``` ctrl+` ```
- add menu item for `Inline code`

* rename menu item Inline Code -> Code
2019-06-10 08:05:20 +01:00
Helmut K. C. Tessarek
2cf1cda128 Update de_DE.po (#1640) 2019-06-10 01:18:07 -04:00
Laurent Cozic
fedee9499b Funding: Remove name for now because it causes an error 2019-06-08 10:38:42 +01:00
Laurent Cozic
7c6c7f34ba Android release v1.0.261 2019-06-08 00:49:41 +01:00
Laurent Cozic
0d65edd0e8 Electron release v1.0.159 2019-06-08 00:47:09 +01:00
Laurent Cozic
d57520c66a Merge branch 'master' of github.com:laurent22/joplin 2019-06-08 00:23:35 +01:00
Laurent Cozic
fa28ae1433 Mobile: Improved and cleaned up config screen 2019-06-08 00:23:17 +01:00
Laurent Cozic
0acf9823e5 Added missing test 2019-06-07 23:39:34 +01:00
Caleb John
799ad5f1da Desktop: Improved tag dialog to make it easier to add and remove tags (#1589) 2019-06-07 23:33:06 +01:00
Laurent Cozic
9f6b3ccf40 All: Improved: Allow using multiple connections to download items while synchronising (#1633)
* Task queue

* All: Improved sync speed by downloading items in parallel

* Improved download queue
2019-06-07 23:20:08 +01:00
Laurent Cozic
df714c357d All: Improved: Better handling of items that cannot be decrypted, including those that cause crashes 2019-06-07 23:11:08 +01:00
Laurent Cozic
de5fdc84f8 Added simple Key-Value store to support temporary data 2019-06-07 08:05:15 +00:00
Laurent Cozic
b5b228af15 Merge branch 'master' of github.com:laurent22/joplin 2019-06-06 16:05:26 +01:00
Laurent Cozic
7a85628f46 Doc: added Twitter feed and Product Hunt page 2019-06-06 16:05:14 +01:00
MichipX
075e76fc4b [Update] Language file: pl_PL.po (#1625)
Addition of strings and correction of mistakes.
2019-06-06 13:40:51 +01:00
Gustavo Reis
172a98fed4 Desktop: Fix icon path and directory in Linux install script (#1612) 2019-06-05 23:14:19 +01:00
Caleb John
409bdd7b78 Desktop, mobile: Upgrade TOC plugin version to 4.0.0 to fix various issues (#1603) 2019-06-05 23:13:00 +01:00
Caleb John
d012c689e1 Desktop: Improve how font size is applied (#1601) 2019-06-05 23:11:18 +01:00
Caleb John
89ec0629e6 Bump version of multimd-tables for mobile (#1597) 2019-06-05 23:02:10 +01:00
Laurent Cozic
b3475ae195 Android release v1.0.260 2019-06-05 18:31:59 +01:00
Laurent Cozic
a72ab67473 CLI v1.0.139 2019-06-05 18:09:10 +01:00
Laurent Cozic
90fbfec914 Update translations 2019-06-05 18:07:11 +01:00
Germán Martín
49ef023a90 Complete Spanish translation (#1613)
* Complete Spanish translation

Add missing translations

* Fix translation

* Review translation with Poedit
2019-06-05 18:02:36 +01:00
abonte
64bfd74f18 Translation: update it_IT.po (#1616)
* update Italian translation

* small fix
2019-06-05 18:02:20 +01:00
Laurent Cozic
655e35056e Merge branch 'master' of github.com:laurent22/joplin 2019-06-05 17:41:40 +01:00
Laurent Cozic
a13ba63ab8 Desktop: New: Added option to open development tools, to make it easier to create custom CSS 2019-06-05 17:41:30 +01:00
Laurent Cozic
e2e00d4c87 Doc: Added info to submit a web clipper bug 2019-06-05 15:09:22 +01:00
Helmut K. C. Tessarek
159dc44f6c Update website 2019-05-28 22:43:54 -04:00
Laurent Cozic
8fe2091926 Desktop, CLI: Fixes #1583: Handle multiple lines in attributes when importing Enex files 2019-05-28 22:52:09 +01:00
Laurent Cozic
c362c38dc0 Merge branch 'master' of github.com:laurent22/joplin 2019-05-28 22:05:25 +01:00
Laurent Cozic
316a52bbc2 All: Improved workflow of downloading and decrypting data during sync 2019-05-28 22:05:11 +01:00
Helmut K. C. Tessarek
0de9f6f944 Merge pull request #1596 from mmahmoudian/Update-Persian-localization
Improved and fixed Persian (fa) translation
2019-05-28 13:48:03 -04:00
Laurent Cozic
3ba021fdd9 Fixed test for Welcome notebook 2019-05-28 18:17:59 +01:00
Laurent Cozic
04c6579f2c All: Fix: Fix issue with revisions being needlessly created when decrypting notes 2019-05-28 18:10:21 +01:00
Mehrad Mahmoudian
bc7bd456a7 [update] Persian (fa) Language 2019-05-28 19:02:02 +03:00
Laurent Cozic
6d7511efbb Electron release v1.0.158 2019-05-27 19:54:04 +01:00
Caleb John
a0fb99d78f Desktop, Mobile: Improved: Enable more options on multimd-table plugin (#1586)
* Update multimd-table and enable options

* Add `options` option to markdown plugins
2019-05-27 19:53:02 +01:00
Laurent Cozic
685a52c2c5 Desktop, Mobile: Fixes #1587: Fix internal note links 2019-05-27 19:49:18 +01:00
Laurent Cozic
fc77419ca1 Desktop: Fixed empty separators in menu 2019-05-27 19:48:09 +01:00
Laurent Cozic
bab3a12e92 Doc: Updated build instructions for Windows 2019-05-27 18:04:24 +01:00
Laurent Cozic
21f0b90f48 Android release v1.0.255 2019-05-26 19:41:20 +01:00
Laurent Cozic
83682ab513 Desktop, Mobile: Improved config screen with dark theme 2019-05-26 19:39:07 +01:00
Laurent Cozic
7b987b5a8f Desktop: Resolves #1575: Make bold text more visible 2019-05-24 23:51:56 +01:00
Laurent Cozic
cb5aa425c8 Updated translations 2019-05-24 23:09:31 +01:00
Laurent Cozic
dd222381dd Translatiobs 2019-05-24 23:08:31 +01:00
Laurent Cozic
d7210811f6 Android release v1.0.254 2019-05-24 17:38:10 +01:00
Laurent Cozic
3a43cfeebf Electron release v1.0.157 2019-05-24 17:35:53 +01:00
Laurent Cozic
31e33c6628 Updated translations 2019-05-24 17:35:44 +01:00
Laurent Cozic
875f8d6997 Electron release v1.0.156 2019-05-24 17:35:04 +01:00
Laurent Cozic
aaaf27af6e Android: Fixed resource loading issue, and improved logging on desktop 2019-05-24 17:34:18 +01:00
Laurent Cozic
43624ffa75 Desktop: Improved: Add number of characters removed and added in revision list 2019-05-24 17:31:18 +01:00
Laurent Cozic
996b6623f1 Android release v1.0.253 2019-05-24 14:50:18 +01:00
Laurent Cozic
613041b806 Electron release v1.0.155 2019-05-24 14:47:35 +01:00
Laurent Cozic
ff1d01a864 Merge branch 'master' of github.com:laurent22/joplin 2019-05-24 14:47:30 +01:00
Laurent Cozic
bcbbe10bf8 Electron release v1.0.154 2019-05-24 14:47:12 +01:00
Laurent Cozic
3de0abfc84 Update FUNDING.yml 2019-05-24 09:57:53 +01:00
Laurent Cozic
133fd03469 Update FUNDING.yml 2019-05-24 09:55:29 +01:00
Laurent Cozic
4f97c5c017 Update FUNDING.yml 2019-05-24 09:55:07 +01:00
Laurent Cozic
2d8fbac58c Create FUNDING.yml 2019-05-24 09:53:31 +01:00
Laurent Cozic
95f7ac4a4a Merge branch 'master' of github.com:laurent22/joplin 2019-05-24 09:07:11 +01:00
Laurent Cozic
6a56a6ccf0 Doc: Added more technical info for revision history 2019-05-24 09:05:16 +01:00
Luis Orozco
1eb8df9fa6 Desktop: Fixes #1186, #1354: Clears search when clicking on a notebook. (#1504)
* Fixes #1186, #1354. Clears search when clicking on a notebook.

* use new resetSearch method where possible, replaced tabs with spaces

* replaced a couple more spaces with tabs
2019-05-24 08:13:01 +01:00
Helmut K. C. Tessarek
485b4baebb Update de_DE.po 2019-05-23 23:40:42 -04:00
Laurent Cozic
5590d887c9 Doc: Added note history info 2019-05-24 00:21:15 +01:00
Laurent Cozic
d00bfa997e Doc: Fixed more links 2019-05-22 16:57:45 +01:00
Laurent Cozic
1a8590e9b9 Update website 2019-05-22 16:49:57 +01:00
Laurent Cozic
5a978977df Fix Iran flag 2019-05-22 16:49:32 +01:00
Laurent Cozic
5d763c7e6c All: Remove tags from Welcome item due to issue with cleaning them up afterwards 2019-05-22 16:38:53 +01:00
Laurent Cozic
050b089e72 Update translations 2019-05-22 16:34:59 +01:00
Helmut K. C. Tessarek
733ea4027c Doc: use new forum link (#1545) 2019-05-22 16:20:10 +01:00
Helmut K. C. Tessarek
10500c78b1 All: Fix: Default sort order for notebooks should be title and ascending (#1541) 2019-05-22 16:18:16 +01:00
Caleb John
0040cc02a2 Only delete the .desktop file if it will be replaced by the script (#1537) 2019-05-22 16:16:41 +01:00
Luis Orozco
74afd20f0c Desktop: Fixes #1426: added backticks to auto-wrapping quotes. (#1534) 2019-05-22 16:16:03 +01:00
Luis Orozco
dc9bde2184 Github: updated Contributing.md (#1533)
* updated Contributing.md

- Added several guidelines
- Moved some rules to bulleted lists (for quicker reading).

* Replace links to old forum domain to new domain. Removed a word.
2019-05-22 16:14:59 +01:00
Mehrad Mahmoudian
5243ea7eb2 All: New: Added Persian translation (#1539)
* [init] the first version of the fa.po file

Providing translation for Persian language

* [fix] moved the fa.po file into correct path
2019-05-22 16:05:25 +01:00
水货
7d93492658 Update zh_CN.po (#1524) 2019-05-22 15:57:20 +01:00
Helmut K. C. Tessarek
c6b56345f5 add /%d to Fetching resources: %d (#1532) 2019-05-22 15:56:25 +01:00
Laurent Cozic
8a6fe20a69 All: Resolves #1481: New: Allow downloading attachments on demand or automatically (#1527)
* Allow downloading resources automatically, on demand, or when loading note

* Make needToBeFetched calls to return the right number of resources

* All: Improved handling of resource downloading and decryption

* Desktop: Click on resource to download it (and, optionally, to decrypt it)

* Desktop: Better handling of resource state (not downloaded, downloading, encrypted) in front end

* Renamed setting to sync.resourceDownloadMode

* Download resources when changing setting

* tweaks

* removed duplicate cs

* Better report resource download progress

* Make sure resource cache is properly cleared when needed

* Also handle manual download for non-image resources

* More improvements to logic when downloading and decrypting resources
2019-05-22 15:56:07 +01:00
Laurent Cozic
6bcbedd6a4 CLI v1.0.137 2019-05-19 12:05:02 +01:00
Laurent Cozic
4c935b78f9 Removed log statement 2019-05-19 12:04:09 +01:00
Laurent Cozic
94cddda6d0 Removed temp files 2019-05-19 11:22:00 +01:00
Laurent Cozic
1924ea062c CLI v1.0.136 2019-05-19 11:20:17 +01:00
Laurent Cozic
07e88b2eeb All: Handle missing resource blob when setting resource size 2019-05-19 11:18:44 +01:00
Laurent Cozic
e4a08c29d7 Desktop, Mobile: Improved: Gray out checkboxes that have been ticked inside notes 2019-05-17 22:41:30 +01:00
Laurent Cozic
d60afcaabe Fixed merge 2019-05-16 17:36:02 +00:00
Laurent Cozic
1a091460ca All: Fixed: Prevent app from trying to upload resource it has not downloaded yet 2019-05-16 17:34:16 +00:00
Laurent Cozic
8ebaa7f6eb All: Put back "Fetched items" message during sync 2019-05-15 08:14:36 +01:00
Laurent Cozic
e2a64e21a2 Electron release v1.0.153 2019-05-14 22:23:47 +01:00
Laurent Cozic
78ddd22f09 Log more revision information to allow debugging issues 2019-05-14 22:23:34 +01:00
Laurent Cozic
c546b7076a Fixed doc 2019-05-14 22:02:47 +01:00
Laurent Cozic
0e2bb5d784 Desktop: Improved: When opening a note using Goto Anything, open all its parent notebooks too 2019-05-14 00:11:27 +01:00
Laurent Cozic
5c069c38f5 CLI v1.0.135 2019-05-13 23:59:27 +01:00
Laurent Cozic
451b9c0ae9 CLI v1.0.133 2019-05-13 23:55:53 +01:00
Laurent Cozic
047897621a Fix CLI build script 2019-05-13 23:52:12 +01:00
Laurent Cozic
52e5cec585 Update website 2019-05-13 23:41:31 +01:00
Laurent Cozic
bc98b65efa Update website 2019-05-13 23:23:57 +01:00
Laurent Cozic
9250e77862 Added link to CLI changelog 2019-05-13 23:23:42 +01:00
Laurent Cozic
cd69e71945 Forgot to publish in publish script 2019-05-13 23:20:25 +01:00
Laurent Cozic
e705e6e990 CLI v1.0.129 2019-05-13 23:18:57 +01:00
Laurent Cozic
4638f11c5e Created CLI release script with changelog auto-generation 2019-05-13 23:18:44 +01:00
Laurent Cozic
9de7c15e93 CLI v1.0.128 2019-05-13 22:53:08 +01:00
Laurent Cozic
61736546b4 Updated translations 2019-05-13 22:52:42 +01:00
Laurent Cozic
82b6dd23a7 CLI v1.0.127 2019-05-13 10:11:10 +01:00
Laurent Cozic
64427f0160 iOS 10.0.34 2019-05-13 10:10:37 +01:00
Laurent Cozic
cbf47cb9ee Android release v1.0.252 2019-05-13 09:53:50 +01:00
Laurent Cozic
dba3e4202d Electron release v1.0.152 2019-05-13 09:51:04 +01:00
Laurent Cozic
173cd6de4d Fixed regression 2019-05-13 09:50:39 +01:00
Laurent Cozic
3d333bd8f2 Removed build files 2019-05-13 00:43:12 +01:00
823 changed files with 50156 additions and 54780 deletions

47
.eslintignore Normal file
View File

@@ -0,0 +1,47 @@
*.min.js
.git/
.github/
_mydocs/
_releases/
Assets/
CliClient/build
CliClient/locales
CliClient/node_modules
CliClient/tests-build
CliClient/tests/enex_to_md
CliClient/tests/html_to_md
CliClient/tests/logs
CliClient/tests/support
CliClient/tests/sync
CliClient/tests/tmp
Clipper/joplin-webclipper/content_scripts/JSDOMParser.js
Clipper/joplin-webclipper/content_scripts/Readability-readerable.js
Clipper/joplin-webclipper/content_scripts/Readability.js
Clipper/joplin-webclipper/dist
Clipper/joplin-webclipper/icons
Clipper/joplin-webclipper/popup/build
Clipper/joplin-webclipper/popup/node_modules
docs/
ElectronClient/app/dist
ElectronClient/app/lib
ElectronClient/app/lib/vendor/sjcl-rn.js
ElectronClient/app/lib/vendor/sjcl.js
ElectronClient/app/locales
ElectronClient/app/node_modules
highlight.pack.js
node_modules/
ReactNativeClient/android
ReactNativeClient/ios
ReactNativeClient/lib/vendor/
ReactNativeClient/lib/welcomeAssets.js
ReactNativeClient/locales
ReactNativeClient/node_modules
readme/
Tools/node_modules
Tools/PortableAppsLauncher
Server/.git/
Server/.github/
Server/docs/
Server/dist/
Server/bin/
Server/node_modules/

62
.eslintrc.js Normal file
View File

@@ -0,0 +1,62 @@
module.exports = {
'env': {
'browser': true,
'es6': true,
'node': true,
},
"parser": "@typescript-eslint/parser",
'extends': ['eslint:recommended'],
'globals': {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly',
// Jasmine variables
'expect': 'readonly',
'describe': 'readonly',
'it': 'readonly',
'beforeEach': 'readonly',
'jasmine': 'readonly',
// React Native variables
'__DEV__': 'readonly',
// Clipper variables
'browserSupportsPromises_': true,
'chrome': 'readonly',
'browser': 'readonly',
},
'parserOptions': {
'ecmaVersion': 2018,
"ecmaFeatures": {
"jsx": true,
},
"sourceType": "module",
},
'rules': {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
// Ignore all unused function arguments, because in some
// case they are kept to indicate the function signature.
//"no-unused-vars": ["error", { "argsIgnorePattern": ".*" }],
"@typescript-eslint/no-unused-vars": ["error"],
"no-constant-condition": 0,
"no-prototype-builtins": 0,
"space-in-parens": ["error", "never"],
"semi": ["error", "always"],
"eol-last": ["error", "always"],
"quotes": ["error", "single"],
"indent": ["error", "tab"],
"comma-dangle": ["error", "always-multiline"],
"no-trailing-spaces": "error",
"linebreak-style": ["error", "unix"],
// This error is always a false positive so far since it detects
// possible race conditions in contexts where we know it cannot happen.
"require-atomic-updates": 0,
"prefer-template": ["error"],
"template-curly-spacing": ["error", "never"]
},
"plugins": [
"react",
"@typescript-eslint",
],
};

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: joplin
github: laurent22
custom: https://joplinapp.org/donate/

4
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,4 @@
👉 Please follow one of these issue templates:
- https://github.com/laurent22/joplin/issues/new/choose
Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates.

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,48 @@
---
name: "🐛 Bug Report"
about: Report a reproducible bug or regression in Joplin.
title: ''
labels: 'bug'
---
<!--
Please provide a clear and concise description of what the bug is. (In the section Steps To Reproduce.)
Include screenshots if needed.
Please test using the latest Joplin release to make sure your issue has not already been fixed.
-->
<!--
IMPORTANT: If you are reporting a clipper bug, please include an example URL that shows the issue.
Without the URL the issue is likely to be closed.
-->
## Environment
Joplin version:
Platform:
OS specifcs:
<!--
Platform can be one of: macOS, Linux, Windows, Android, iOS, terminal (or a combination)
OS specifcs: e.g. OS version, Linux distribution, Android/iOS version, ...
-->
## Steps To Reproduce
1.
2.
<!--
Issues without reproduction steps are likely to stall.
-->
Describe what you expected to happen:
## Logfile
<!--
Please attach a debug log. Issues without a debug log are likely to stall.
For information on how to collect a log file: https://joplinapp.org/debugging/
-->

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Report an accepted feature request.
title: ''
labels: 'feature request'
---
<!--
Please search open issues first - many features have already been requested!
-->
🚨 A feature request that has not been accepted on the forum will be closed! 🚨
## Has it been discussed in the forum? Link to topic.
<!--
Feature requests must be discussed and accepted in the forum first. https://discourse.joplinapp.org
Please provide a link to the topic.
Feature requests without a link to the discussion/topic on the forum will be closed.
-->

29
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: "🤔 Questions and Help"
about: The issue tracker is not for questions. Please ask questions on https://discourse.joplinapp.org/.
title: ''
labels: 'question'
---
🚨 The issue tracker is not for questions. 🚨
As it happens, support requests that are created as issues are likely to be closed. We want to make sure you are able to find the help you seek.
## Questions and Help
Please read the [documentation](https://joplinapp.org/) and [FAQ](https://joplinapp.org/faq/) first.
### https://discourse.joplinapp.org/
If you have still questions related to Joplin, please open a topic in the [forum](https://discourse.joplinapp.org/).
You can use your GitHub credentials to login to the forum.
## Links
- Documentation: https://joplinapp.org
- FAQ: https://joplinapp.org/faq/
- Forum: https://discourse.joplinapp.org
- How to enable end-to-end encryption: https://joplinapp.org/e2ee/
- API documentation: https://joplinapp.org/api/
- How to enable debug mode: https://joplinapp.org/debugging/

25
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "good first issue"
- "essential"
- "essential-reviewed"
- "help wanted"
- "nice to have"
- "upstream"
- "backlog"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs.
You may also label this issue as "backlog" and I will leave it open.
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.
only: issues

3
.gitignore vendored
View File

@@ -42,4 +42,5 @@ ReactNativeClient/lib/csstojs/
ReactNativeClient/lib/rnInjectedJs/
ElectronClient/app/gui/note-viewer/fonts/
ElectronClient/app/gui/note-viewer/lib.js
Tools/commit_hook.txt
Tools/commit_hook.txt
.vscode/*

View File

@@ -1,5 +1,5 @@
# Only build tags (Doesn't work - doesn't build anything)
if: tag IS present
if: tag IS present OR type = pull_request
rvm: 2.3.3
@@ -54,8 +54,20 @@ before_install:
script:
- |
# Install tools
cd Tools
npm install
# Run test units
cd ../CliClient
npm install
./run_test.sh
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
# Prepare the Electron app and build it
cd ../ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && yarn dist
npm install && USE_HARD_LINKS=false yarn dist

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

BIN
Assets/AdresseSupport.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -3,6 +3,7 @@
# General information
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
## macOS dependencies
@@ -18,11 +19,10 @@
# Building the tools
Before building any of the applications, you need to build the tools:
Before building any of the applications, you need to build the tools and pre-commit hooks:
```
cd Tools
npm install
npm install && cd Tools && npm install
```
# Building the Electron application
@@ -59,6 +59,8 @@ If node-gyp does not works (MSBUILD: error MSB3428: Could not load the Visual C+
If `yarn dist` fails, it may need administrative rights.
If you get an `error MSB8020: The build tools for v140 cannot be found.` try to run with a different toolset version, eg `npm install --toolset=v141` (See [here](https://github.com/mapbox/node-sqlite3/issues/1124) for more info).
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
# Building the Mobile application
@@ -76,4 +78,4 @@ npm install
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
```
Run `run.sh` to start the application for testing.
Run `run.sh` to start the application for testing.

View File

@@ -1,28 +1,73 @@
# User support
For general discussion about Joplin, user support, software development questions, and to discuss new features, please go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
The [Joplin Forum](https://discourse.joplinapp.org/) is the community driven place for user support, general discussion about Joplin, problems with installation, new features and software development questions. It is possible to login with your GitHub account. Don't use the issue tracker for support questions.
# 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.
File bugs in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Please follow these guidelines:
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.
- Search existing issues first, make sure yours hasn't already been reported.
- Consider [enabling debug mode](https://joplinapp.org/debugging/) so that you can provide as much details as possible when reporting the issue.
- Stay on topic, but describe the issue in detail so that others can reproduce it.
- **Provide a screenshot** if possible. A screenshot showing the problem is often more useful than a paragraph describing it.
- For web clipper bugs, **please provide the URL causing the issue**. Sometimes the clipper works in one page but not in another so it is important to know what URL has a problem.
# 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. "+1" comments are not tracked.
Please check that your request has not already been posted in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). If it has, **up-voting the issue** increases the chances it'll be noticed and implemented in the future. "+1" comments are not tracked.
# Creating a pull request
As a general rule, suggestions to *improve Joplin* should be posted first in the [Joplin Forum](https://discourse.joplinapp.org/) for discussion.
- If you want to add a new feature, consider asking about it before implementing it or checking existing discussions to make sure it is within the scope of the project. As a rule of thumb **if your change is likely to involve more than 50 lines of code, you should discuss it in the forum**, just so that you don't spend too much time implementing something that might not be accepted.
Avoid listing multiple requests in one report in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). One issue per request makes it easier to track and discuss it.
- Bug fixes are always welcome.
Finally, when submitting a pull request, don't forget to [test your code](#unit-tests).
# Contribute to the project
## Contributing to Joplin's translation
Joplin is available in multiple languages thanks to the help of its users. You can help translate Joplin to your language or keep it up to date. Please read the documentation about [Localisation](https://joplinapp.org/#localisation).
## Contributing to Joplin's code
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
- Bug fixes are always welcome. Start by reviewing the list of [essential issues](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Aessential)
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
- **Changes that will consist in more than 50 lines of code should be discussed the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
# Coding style
## Coding style
There are only two rules, but not following them means the pull request will not be accepted (it can be accepted once the issues are fixed):
Coding style is enforced by a pre-commit hook that runs eslint. This hook is installed whenever running `npm install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `npm install` at the root of the repository.
- **Please use tabs, NOT spaces.**
- **Please do not add or remove optional characters, such as spaces or colons.** Please setup your editor so that it only changes what you are working on and is not making automated changes elsewhere. The reason for this is that small white space changes make diff hard to read and can cause needless conflicts.
## Unit tests
When submitting a pull request for a new feature or bug fix, please add unit tests for your code. Unit testing GUI changes is not always possible so it is not required, but any change in a file under /lib for example should be unit tested.
The tests are under CliClient/tests. To get them running, you first need to build the CLI app:
cd CliClient
npm i
To run the test units, you must have an instance of the cli app running. In a first window navigate into `CliClient` and run:
```sh
./run.sh
```
> If you get an error like `Error: Cannot find module '../locales/index.js'`, this means you must (a) rebuild translations or (b) take > them from one of the other apps. To do option b, you can run the following command to copy them from the `ReactNativeClient` directory:>
>
> ```sh
> cd .. # Return to the root of the project
> rsync -aP ./ReactNativeClient/locales/ ./CliClient/build/locales/
> ```
Then run the tests in a second window. To run all the test units:
./run_test.sh
To run just one particular file:
./run_test.sh markdownUtils # Don't add the .js extension

13
CliClient/.eslintrc.js Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
"overrides": [
{
"files": ["tests/**/*.js"],
'rules': {
// Ignore all unused function arguments, because in some
// case they are kept to indicate the function signature.
"no-unused-vars": ["error", { "argsIgnorePattern": ".*" }],
"@typescript-eslint/no-unused-vars": 0,
}
},
],
};

View File

@@ -1,14 +1,11 @@
const { _ } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const Resource = require('lib/models/Resource.js');
const { netUtils } = require('lib/net-utils.js');
const http = require("http");
const urlParser = require("url");
const http = require('http');
const urlParser = require('url');
const enableServerDestroy = require('server-destroy');
class ResourceServer {
constructor() {
this.server_ = null;
this.logger_ = new Logger();
@@ -31,7 +28,7 @@ class ResourceServer {
baseUrl() {
if (!this.port_) return '';
return 'http://127.0.0.1:' + this.port_;
return `http://127.0.0.1:${this.port_}`;
}
setLinkHandler(handler) {
@@ -40,7 +37,7 @@ class ResourceServer {
async start() {
this.port_ = await netUtils.findAvailablePort([9167, 9267, 8167, 8267]);
if (!this.port_) {
if (!this.port_) {
this.logger().error('Could not find available port to start resource server. Please report the error at https://github.com/laurent22/joplin');
return;
}
@@ -48,16 +45,15 @@ class ResourceServer {
this.server_ = http.createServer();
this.server_.on('request', async (request, response) => {
const writeResponse = (message) => {
const writeResponse = message => {
response.write(message);
response.end();
}
};
const url = urlParser.parse(request.url, true);
let resourceId = url.pathname.split('/');
if (resourceId.length < 2) {
writeResponse('Error: could not get resource ID from path name: ' + url.pathname);
writeResponse(`Error: could not get resource ID from path name: ${url.pathname}`);
return;
}
resourceId = resourceId[1];
@@ -66,9 +62,10 @@ class ResourceServer {
try {
const done = await this.linkHandler_(resourceId, response);
if (!done) throw new Error('Unhandled resource: ' + resourceId);
if (!done) throw new Error(`Unhandled resource: ${resourceId}`);
} catch (error) {
response.setHeader('Content-Type', 'text/plain');
// eslint-disable-next-line require-atomic-updates
response.statusCode = 400;
response.write(error.message);
}
@@ -76,7 +73,7 @@ class ResourceServer {
response.end();
});
this.server_.on('error', (error) => {
this.server_.on('error', error => {
this.logger().error('Resource server:', error);
});
@@ -91,7 +88,6 @@ class ResourceServer {
if (this.server_) this.server_.destroy();
this.server_ = null;
}
}
module.exports = ResourceServer;
module.exports = ResourceServer;

View File

@@ -5,13 +5,12 @@ const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const { cliUtils } = require('./cli-utils.js');
const { reducer, defaultState } = require('lib/reducer.js');
const { splitCommandString } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const htmlentities = new Entities().encode;
const chalk = require('chalk');
const tk = require('terminal-kit');
@@ -20,12 +19,10 @@ const Renderer = require('tkwidgets/framework/Renderer.js');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseWidget = require('tkwidgets/BaseWidget.js');
const ListWidget = require('tkwidgets/ListWidget.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const HLayoutWidget = require('tkwidgets/HLayoutWidget.js');
const VLayoutWidget = require('tkwidgets/VLayoutWidget.js');
const ReduxRootWidget = require('tkwidgets/ReduxRootWidget.js');
const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js');
const NoteWidget = require('./gui/NoteWidget.js');
@@ -37,7 +34,6 @@ const StatusBarWidget = require('./gui/StatusBarWidget.js');
const ConsoleWidget = require('./gui/ConsoleWidget.js');
class AppGui {
constructor(app, store, keymap) {
try {
this.app_ = app;
@@ -50,12 +46,12 @@ class AppGui {
// 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',
focus_next: 'TAB',
focus_previous: 'SHIFT_TAB',
move_up: 'UP',
move_down: 'DOWN',
page_down: 'PAGE_DOWN',
page_up: 'PAGE_UP',
};
this.renderer_ = null;
@@ -64,7 +60,7 @@ class AppGui {
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.app_.on('modelAction', async (event) => {
this.app_.on('modelAction', async event => {
await this.handleModelAction(event.action);
});
@@ -83,7 +79,7 @@ class AppGui {
reg.setupRecurrentSync();
DecryptionWorker.instance().scheduleStart();
} catch (error) {
this.fullScreen(false);
if (this.term_) { this.fullScreen(false); }
console.error(error);
process.exit(1);
}
@@ -134,7 +130,7 @@ class AppGui {
};
folderList.name = 'folderList';
folderList.vStretch = true;
folderList.on('currentItemChange', async (event) => {
folderList.on('currentItemChange', async event => {
const item = folderList.currentItem;
if (item === '-') {
@@ -169,7 +165,7 @@ class AppGui {
});
}
});
this.rootWidget_.connect(folderList, (state) => {
this.rootWidget_.connect(folderList, state => {
return {
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
@@ -196,7 +192,7 @@ class AppGui {
id: note ? note.id : null,
});
});
this.rootWidget_.connect(noteList, (state) => {
this.rootWidget_.connect(noteList, state => {
return {
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
items: state.notes,
@@ -210,7 +206,7 @@ class AppGui {
borderBottomWidth: 1,
borderLeftWidth: 1,
};
this.rootWidget_.connect(noteText, (state) => {
this.rootWidget_.connect(noteText, state => {
return {
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
notes: state.notes,
@@ -225,7 +221,7 @@ class AppGui {
borderLeftWidth: 1,
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
this.rootWidget_.connect(noteMetadata, state => {
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
@@ -292,7 +288,7 @@ class AppGui {
if (!cmd) return;
const isConfigPassword = cmd.indexOf('config ') >= 0 && cmd.indexOf('password') >= 0;
if (isConfigPassword) return;
this.stdout(chalk.cyan.bold('> ' + cmd));
this.stdout(chalk.cyan.bold(`> ${cmd}`));
}
setupKeymap(keymap) {
@@ -301,7 +297,7 @@ class AppGui {
for (let i = 0; i < keymap.length; i++) {
const item = Object.assign({}, keymap[i]);
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
if (!item.command) throw new Error(`Missing command for keymap item: ${JSON.stringify(item)}`);
if (!('type' in item)) item.type = 'exec';
@@ -408,7 +404,7 @@ class AppGui {
activeListItem() {
const widget = this.widget('mainWindow').focusedWidget;
if (!widget) return null;
if (widget.name == 'noteList' || widget.name == 'folderList') {
return widget.currentItem;
}
@@ -430,25 +426,21 @@ class AppGui {
}
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);
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) {
@@ -462,9 +454,7 @@ class AppGui {
} 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();
@@ -475,22 +465,15 @@ class AppGui {
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);
await this.processPromptCommand(cmd);
} else {
throw new Error('Unknown command: ' + cmd);
throw new Error(`Unknown command: ${cmd}`);
}
}
@@ -501,7 +484,7 @@ class AppGui {
// this.logger().debug('Got command: ' + cmd);
try {
try {
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
let args = splitCommandString(cmd);
@@ -511,7 +494,7 @@ class AppGui {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}
@@ -523,7 +506,7 @@ class AppGui {
}
this.widget('console').scrollBottom();
// Invalidate so that the screen is redrawn in case inputting a command has moved
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
this.widget('root').invalidate();
@@ -603,17 +586,17 @@ class AppGui {
async setupResourceServer() {
const linkStyle = chalk.blue.underline;
const noteTextWidget = this.widget('noteText');
const resourceIdRegex = /^:\/[a-f0-9]+$/i
const resourceIdRegex = /^:\/[a-f0-9]+$/i;
const noteLinks = {};
const hasProtocol = function(s, protocols) {
if (!s) return false;
s = s.trim().toLowerCase();
for (let i = 0; i < protocols.length; i++) {
if (s.indexOf(protocols[i] + '://') === 0) return true;
if (s.indexOf(`${protocols[i]}://`) === 0) return true;
}
return false;
}
};
// By default, before the server is started, only the regular
// URLs appear in blue.
@@ -637,29 +620,31 @@ class AppGui {
const link = noteLinks[path];
if (link.type === 'url') {
response.writeHead(302, { 'Location': link.url });
response.writeHead(302, { Location: link.url });
return true;
}
if (link.type === 'item') {
const itemId = link.id;
let item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error('No item with ID ' + itemId); // Should be nearly impossible
if (!item) throw new Error(`No item with ID ${itemId}`); // Should be nearly impossible
if (item.type_ === BaseModel.TYPE_RESOURCE) {
if (item.mime) response.setHeader('Content-Type', item.mime);
response.write(await Resource.content(item));
} else if (item.type_ === BaseModel.TYPE_NOTE) {
const html = [`
const html = [
`
<!DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head><meta charset="UTF-8"/></head><body>
`];
html.push('<pre>' + htmlentities(item.title) + '\n\n' + htmlentities(item.body) + '</pre>');
`,
];
html.push(`<pre>${htmlentities(item.title)}\n\n${htmlentities(item.body)}</pre>`);
html.push('</body></html>');
response.write(html.join(''));
} else {
throw new Error('Unsupported item type: ' + item.type_);
throw new Error(`Unsupported item type: ${item.type_}`);
}
return true;
@@ -679,7 +664,7 @@ class AppGui {
noteLinks[index] = {
type: 'item',
id: url.substr(2),
};
};
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {
noteLinks[index] = {
type: 'url',
@@ -691,7 +676,7 @@ class AppGui {
return url;
}
return linkStyle(this.resourceServer_.baseUrl() + '/' + index);
return linkStyle(`${this.resourceServer_.baseUrl()}/${index}`);
},
};
}
@@ -710,8 +695,7 @@ class AppGui {
term.grabInput();
term.on('key', async (name, matches, data) => {
term.on('key', async (name) => {
// -------------------------------------------------------------------------
// Handle special shortcuts
// -------------------------------------------------------------------------
@@ -729,13 +713,13 @@ class AppGui {
return;
}
if (name === 'CTRL_C' ) {
if (name === 'CTRL_C') {
const cmd = this.app().currentCommand();
if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) {
this.stdout(_('Press Ctrl+D or type "exit" to exit the application'));
} else {
this.commandCancelCalled_ = true;
await cmd.cancel()
await cmd.cancel();
this.commandCancelCalled_ = false;
}
return;
@@ -744,8 +728,8 @@ class AppGui {
// -------------------------------------------------------------------------
// Build up current shortcut
// -------------------------------------------------------------------------
const now = (new Date()).getTime();
const now = new Date().getTime();
if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) {
this.currentShortcutKeys_ = [name];
@@ -793,7 +777,7 @@ class AppGui {
} else if (keymapItem.type === 'tkwidgets') {
this.widget('root').handleKey(this.tkWidgetKeys_[keymapItem.command]);
} else {
throw new Error('Unknown command type: ' + JSON.stringify(keymapItem));
throw new Error(`Unknown command type: ${JSON.stringify(keymapItem)}`);
}
}
@@ -813,7 +797,6 @@ class AppGui {
process.exit(1);
});
}
}
AppGui.INPUT_MODE_NORMAL = 1;

View File

@@ -1,10 +1,5 @@
const { BaseApplication } = require('lib/BaseApplication');
const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState } = require('lib/reducer.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { Database } = require('lib/database.js');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const ResourceService = require('lib/services/ResourceService');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
@@ -12,21 +7,15 @@ const BaseItem = require('lib/models/BaseItem.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const Setting = require('lib/models/Setting.js');
const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const Cache = require('lib/Cache');
const WelcomeUtils = require('lib/WelcomeUtils');
const RevisionService = require('lib/services/RevisionService');
class Application extends BaseApplication {
constructor() {
super();
@@ -75,7 +64,7 @@ class Application extends BaseApplication {
// const response = await cliUtils.promptMcq(msg, answers);
// if (!response) return null;
return output[response - 1];
// return output[response - 1];
} else {
return output.length ? output[0] : null;
}
@@ -97,10 +86,12 @@ class Application extends BaseApplication {
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) { // Handle it as pattern
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
// Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else { // Single item
} else {
// Single item
let item = null;
if (type == BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
@@ -126,15 +117,15 @@ class Application extends BaseApplication {
}
setupCommand(cmd) {
cmd.setStdout((text) => {
cmd.setStdout(text => {
return this.stdout(text);
});
cmd.setDispatcher((action) => {
cmd.setDispatcher(action => {
if (this.store()) {
return this.store().dispatch(action);
} else {
return (action) => {};
return () => {};
}
});
@@ -145,10 +136,10 @@ class Application extends BaseApplication {
if (!options.answers) options.answers = options.booleanAnswerDefault === 'y' ? [_('Y'), _('n')] : [_('N'), _('y')];
if (options.type == 'boolean') {
message += ' (' + options.answers.join('/') + ')';
message += ` (${options.answers.join('/')})`;
}
let answer = await this.gui().prompt('', message + ' ', options);
let answer = await this.gui().prompt('', `${message} `, options);
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
@@ -185,12 +176,12 @@ class Application extends BaseApplication {
commands(uiType = null) {
if (!this.allCommandsLoaded_) {
fs.readdirSync(__dirname).forEach((path) => {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
const ext = fileExtension(path);
if (ext != 'js') return;
let CommandClass = require('./' + path);
let CommandClass = require(`./${path}`);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
@@ -257,7 +248,7 @@ class Application extends BaseApplication {
let CommandClass = null;
try {
CommandClass = require(__dirname + '/command-' + name + '.js');
CommandClass = require(`${__dirname}/command-${name}.js`);
} catch (error) {
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
let e = new Error(_('No such command: %s', name));
@@ -276,19 +267,27 @@ class Application extends BaseApplication {
dummyGui() {
return {
isDummy: () => { return true; },
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
isDummy: () => {
return true;
},
prompt: (initialText = '', promptString = '', options = null) => {
return cliUtils.prompt(initialText, promptString, options);
},
showConsole: () => {},
maximizeConsole: () => {},
stdout: (text) => { console.info(text); },
fullScreen: (b=true) => {},
stdout: text => {
console.info(text);
},
fullScreen: () => {},
exit: () => {},
showModalOverlay: (text) => {},
showModalOverlay: () => {},
hideModalOverlay: () => {},
stdoutMaxWidth: () => { return 100; },
stdoutMaxWidth: () => {
return 100;
},
forceRender: () => {},
termSaveState: () => {},
termRestoreState: (state) => {},
termRestoreState: () => {},
};
}
@@ -300,7 +299,7 @@ class Application extends BaseApplication {
let outException = null;
try {
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
await this.activeCommand_.action(cmdArgs);
} catch (error) {
@@ -316,24 +315,24 @@ class Application extends BaseApplication {
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 }
{ 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
@@ -341,10 +340,10 @@ class Application extends BaseApplication {
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i];
}
const filePath = Setting.value('profileDir') + '/keymap.json';
const filePath = `${Setting.value('profileDir')}/keymap.json`;
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
@@ -356,7 +355,7 @@ class Application extends BaseApplication {
}
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load keymap ' + filePath + '\n' + msg;
msg = `Could not load keymap ${filePath}\n${msg}`;
error.message = msg;
throw error;
}
@@ -374,12 +373,10 @@ class Application extends BaseApplication {
async start(argv) {
argv = await super.start(argv);
cliUtils.setStdout((object) => {
cliUtils.setStdout(object => {
return this.stdout(object);
});
await WelcomeUtils.install(this.dispatch.bind(this));
// If we have some arguments left at this point, it's a command
// so execute it.
if (argv.length) {
@@ -387,6 +384,8 @@ class Application extends BaseApplication {
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
await this.applySettingsSideEffects();
try {
await this.execCommand(argv);
} catch (error) {
@@ -403,7 +402,8 @@ class Application extends BaseApplication {
// Need to call exit() explicitely, otherwise Node wait for any timeout to complete
// https://stackoverflow.com/questions/18050095
process.exit(0);
} else { // Otherwise open the GUI
} else {
// Otherwise open the GUI
this.initRedux();
const keymap = await this.loadKeymaps();
@@ -423,7 +423,7 @@ class Application extends BaseApplication {
const tags = await Tag.allWithNotes();
ResourceService.runInBackground();
RevisionService.instance().runInBackground();
this.dispatch({
@@ -437,7 +437,6 @@ class Application extends BaseApplication {
});
}
}
}
let application_ = null;
@@ -448,4 +447,4 @@ function app() {
return application_;
}
module.exports = { app };
module.exports = { app };

View File

@@ -14,11 +14,11 @@ async function handleAutocompletionPromise(line) {
//should look for commmands it could be
if (words.length == 1) {
if (names.indexOf(words[0]) === -1) {
let x = names.filter((n) => n.indexOf(words[0]) === 0);
let x = names.filter(n => n.indexOf(words[0]) === 0);
if (x.length === 1) {
return x[0] + ' ';
return `${x[0]} `;
}
return x.length > 0 ? x.map((a) => a + ' ') : line;
return x.length > 0 ? x.map(a => `${a} `) : line;
} else {
return line;
}
@@ -34,9 +34,9 @@ async function handleAutocompletionPromise(line) {
let next = words.length > 1 ? words[words.length - 1] : '';
let l = [];
if (next[0] === '-') {
for (let i = 0; i<metadata.options.length; i++) {
for (let i = 0; i < metadata.options.length; i++) {
const options = metadata.options[i][0].split(' ');
//if there are multiple options then they will be separated by comma and
//if there are multiple options then they will be separated by comma and
//space. The comma should be removed
if (options[0][options[0].length - 1] === ',') {
options[0] = options[0].slice(0, -1);
@@ -55,44 +55,43 @@ async function handleAutocompletionPromise(line) {
if (l.length === 0) {
return line;
}
let ret = l.map(a=>toCommandLine(a));
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
let ret = l.map(a => toCommandLine(a));
ret.prefix = `${toCommandLine(words.slice(0, -1))} `;
return ret;
}
//Complete an argument
//Determine the number of positional arguments by counting the number of
//words that don't start with a - less one for the command name
const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1;
const positionalArgs = words.filter(a => a.indexOf('-') !== 0).length - 1;
let cmdUsage = yargParser(metadata.usage)['_'];
cmdUsage.splice(0, 1);
if (cmdUsage.length >= positionalArgs) {
let argName = cmdUsage[positionalArgs - 1];
argName = cliUtils.parseCommandArg(argName).name;
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
l.push(...notes.map((n) => n.title));
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: `${next}*` }) : [];
l.push(...notes.map(n => n.title));
}
if (argName == 'notebook') {
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...folders.map((n) => n.title));
const folders = await Folder.search({ titlePattern: `${next}*` });
l.push(...folders.map(n => n.title));
}
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
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));
let tags = await Tag.search({ titlePattern: `${next}*` });
l.push(...tags.map(n => n.title));
}
if (argName == 'file') {
@@ -113,12 +112,11 @@ async function handleAutocompletionPromise(line) {
if (l.length === 1) {
return toCommandLine([...words.slice(0, -1), l[0]]);
} else if (l.length > 1) {
let ret = l.map(a=>toCommandLine(a));
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
let ret = l.map(a => toCommandLine(a));
ret.prefix = `${toCommandLine(words.slice(0, -1))} `;
return ret;
}
return line;
}
function handleAutocompletion(str, callback) {
handleAutocompletionPromise(str).then(function(res) {
@@ -127,22 +125,24 @@ function handleAutocompletion(str, callback) {
}
function toCommandLine(args) {
if (Array.isArray(args)) {
return args.map(function(a) {
if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
return "'" + a + "'";
} else if (a.indexOf("'") !== -1) {
return '"' + a + '"';
} else {
return a;
}
}).join(' ');
return args
.map(function(a) {
if (a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
return `'${a}'`;
} else if (a.indexOf('\'') !== -1) {
return `"${a}"`;
} else {
return a;
}
})
.join(' ');
} else {
if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
return "'" + args + "' ";
} else if (args.indexOf("'") !== -1) {
return '"' + args + '" ';
if (args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
return `'${args}' `;
} else if (args.indexOf('\'') !== -1) {
return `"${args}" `;
} else {
return args + ' ';
return `${args} `;
}
}
}
@@ -151,9 +151,9 @@ function getArguments(line) {
let inDoubleQuotes = false;
let currentWord = '';
let parsed = [];
for(let i = 0; i<line.length; i++) {
if(line[i] === '"') {
if(inDoubleQuotes) {
for (let i = 0; i < line.length; i++) {
if (line[i] === '"') {
if (inDoubleQuotes) {
inDoubleQuotes = false;
//maybe push word to parsed?
//currentWord += '"';
@@ -161,8 +161,8 @@ function getArguments(line) {
inDoubleQuotes = true;
//currentWord += '"';
}
} else if(line[i] === "'") {
if(inSingleQuotes) {
} else if (line[i] === '\'') {
if (inSingleQuotes) {
inSingleQuotes = false;
//maybe push word to parsed?
//currentWord += "'";
@@ -170,8 +170,7 @@ function getArguments(line) {
inSingleQuotes = true;
//currentWord += "'";
}
} else if (/\s/.test(line[i]) &&
!(inDoubleQuotes || inSingleQuotes)) {
} else if (/\s/.test(line[i]) && !(inDoubleQuotes || inSingleQuotes)) {
if (currentWord !== '') {
parsed.push(currentWord);
currentWord = '';

View File

@@ -2,7 +2,6 @@ const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
class BaseCommand {
constructor() {
this.stdout_ = null;
this.prompt_ = null;
@@ -20,7 +19,7 @@ class BaseCommand {
throw new Error('Description not defined');
}
async action(args) {
async action() {
throw new Error('Action not defined');
}
@@ -93,7 +92,6 @@ class BaseCommand {
logger() {
return reg.logger();
}
}
module.exports = { BaseCommand };
module.exports = { BaseCommand };

View File

@@ -1,7 +1,7 @@
const fs = require('fs-extra');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const { fileExtension, dirname } = require('lib/path-utils.js');
const wrap_ = require('word-wrap');
const { _, setLocale, languageCode } = require('lib/locale.js');
const { languageCode } = require('lib/locale.js');
const rootDir = dirname(dirname(__dirname));
const MAX_WIDTH = 78;
@@ -22,14 +22,14 @@ function renderOptions(options) {
let option = options[i];
const flag = option[0];
const indent = INDENT + INDENT + ' '.repeat(optionColWidth + 2);
let r = wrap(option[1], indent);
r = r.substr(flag.length + (INDENT + INDENT).length);
r = INDENT + INDENT + flag + r;
output.push(r);
}
return output.join("\n");
return output.join('\n');
}
function renderCommand(cmd) {
@@ -44,17 +44,17 @@ function renderCommand(cmd) {
output.push('');
output.push(optionString);
}
return output.join("\n");
return output.join('\n');
}
function getCommands() {
let output = [];
fs.readdirSync(__dirname).forEach((path) => {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
const ext = fileExtension(path);
if (ext != 'js') return;
let CommandClass = require('./' + path);
let CommandClass = require(`./${path}`);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
if (cmd.hidden()) return;
@@ -87,14 +87,14 @@ function getHeader() {
let description = [];
description.push('Joplin is a note taking and to-do application, which can handle a large number of notes organised into notebooks.');
description.push('The notes are searchable, can be copied, tagged and modified with your own text editor.');
description.push("\n\n");
description.push('\n\n');
description.push('The notes can be synchronised with various target including the file system (for example with a network directory) or with Microsoft OneDrive.');
description.push("\n\n");
description.push('\n\n');
description.push('Notes exported from Evenotes via .enex files can be imported into Joplin, including the formatted content, resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).');
output.push(wrap(description.join(''), INDENT));
return output.join("\n");
return output.join('\n');
}
function getFooter() {
@@ -102,18 +102,18 @@ function getFooter() {
output.push('WEBSITE');
output.push('');
output.push(INDENT + 'https://joplinapp.org');
output.push(`${INDENT}https://joplinapp.org`);
output.push('');
output.push('LICENSE');
output.push('');
let filePath = rootDir + '/LICENSE_' + languageCode();
if (!fs.existsSync(filePath)) filePath = rootDir + '/LICENSE';
let filePath = `${rootDir}/LICENSE_${languageCode()}`;
if (!fs.existsSync(filePath)) filePath = `${rootDir}/LICENSE`;
const licenseText = fs.readFileSync(filePath, 'utf8');
output.push(wrap(licenseText, INDENT));
return output.join("\n");
return output.join('\n');
}
async function main() {
@@ -128,12 +128,12 @@ async function main() {
}
const headerText = getHeader();
const commandsText = commandBlocks.join("\n\n");
const commandsText = commandBlocks.join('\n\n');
const footerText = getFooter();
console.info(headerText + "\n\n" + 'USAGE' + "\n\n" + commandsText + "\n\n" + footerText);
console.info(`${headerText}\n\n` + 'USAGE' + `\n\n${commandsText}\n\n${footerText}`);
}
main().catch((error) => {
main().catch(error => {
console.error(error);
});
});

View File

@@ -1,4 +1,4 @@
"use strict"
'use strict';
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
@@ -10,14 +10,14 @@ const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec
const exec = require('child_process').exec;
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection', p, 'reason:', reason);
});
const baseDir = dirname(__dirname) + '/tests/cli-integration';
const joplinAppPath = __dirname + '/main.js';
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
const joplinAppPath = `${__dirname}/main.js`;
const logger = new Logger();
logger.addTarget('console');
@@ -32,17 +32,17 @@ db.setLogger(dbLogger);
function createClient(id) {
return {
'id': id,
'profileDir': baseDir + '/client' + id,
id: id,
profileDir: `${baseDir}/client${id}`,
};
}
const client = createClient(1);
function execCommand(client, command, options = {}) {
let exePath = 'node ' + joplinAppPath;
let cmd = exePath + ' --update-geolocation-disabled --env dev --profile ' + client.profileDir + ' ' + command;
logger.info(client.id + ': ' + command);
function execCommand(client, command) {
let exePath = `node ${joplinAppPath}`;
let cmd = `${exePath} --update-geolocation-disabled --env dev --profile ${client.profileDir} ${command}`;
logger.info(`${client.id}: ${command}`);
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
@@ -72,14 +72,7 @@ function assertEquals(expected, real) {
}
async function clearDatabase() {
await db.transactionExecBatch([
'DELETE FROM folders',
'DELETE FROM notes',
'DELETE FROM tags',
'DELETE FROM note_tags',
'DELETE FROM resources',
'DELETE FROM deleted_items',
]);
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
}
const testUnits = {};
@@ -101,7 +94,7 @@ testUnits.testFolders = async () => {
folders = await Folder.all();
assertEquals(0, folders.length);
}
};
testUnits.testNotes = async () => {
await execCommand(client, 'mkbook nb1');
@@ -121,16 +114,16 @@ testUnits.testNotes = async () => {
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, "rm -f 'blabla*'");
await execCommand(client, 'rm -f \'blabla*\'');
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, "rm -f 'n*'");
await execCommand(client, 'rm -f \'n*\'');
notes = await Note.all();
assertEquals(0, notes.length);
}
};
testUnits.testCat = async () => {
await execCommand(client, 'mkbook nb1');
@@ -145,7 +138,7 @@ testUnits.testCat = async () => {
r = await execCommand(client, 'cat -v mynote');
assertTrue(r.indexOf(note.id) >= 0);
}
};
testUnits.testConfig = async () => {
await execCommand(client, 'config editor vim');
@@ -159,7 +152,7 @@ testUnits.testConfig = async () => {
let r = await execCommand(client, 'config');
assertTrue(r.indexOf('editor') >= 0);
assertTrue(r.indexOf('subl') >= 0);
}
};
testUnits.testCp = async () => {
await execCommand(client, 'mkbook nb2');
@@ -180,7 +173,7 @@ testUnits.testCp = async () => {
notes = await Note.previews(f2.id);
assertEquals(1, notes.length);
assertEquals(notesF1[0].title, notes[0].title);
}
};
testUnits.testLs = async () => {
await execCommand(client, 'mkbook nb1');
@@ -190,7 +183,7 @@ testUnits.testLs = async () => {
assertTrue(r.indexOf('note1') >= 0);
assertTrue(r.indexOf('note2') >= 0);
}
};
testUnits.testMv = async () => {
await execCommand(client, 'mkbook nb2');
@@ -210,21 +203,21 @@ testUnits.testMv = async () => {
await execCommand(client, 'mknote note2');
await execCommand(client, 'mknote note3');
await execCommand(client, 'mknote blabla');
await execCommand(client, "mv 'note*' nb2");
await execCommand(client, 'mv \'note*\' nb2');
notes1 = await Note.previews(f1.id);
notes2 = await Note.previews(f2.id);
assertEquals(1, notes1.length);
assertEquals(4, notes2.length);
}
};
async function main(argv) {
async function main() {
await fs.remove(baseDir);
logger.info(await execCommand(client, 'version'));
await db.open({ name: client.profileDir + '/database.sqlite' });
await db.open({ name: `${client.profileDir}/database.sqlite` });
BaseModel.db_ = db;
await Setting.load();
@@ -237,13 +230,13 @@ async function main(argv) {
await clearDatabase();
let testName = n.substr(4).toLowerCase();
process.stdout.write(testName + ': ');
process.stdout.write(`${testName}: `);
await testUnits[n]();
console.info('');
}
}
main(process.argv).catch((error) => {
main(process.argv).catch(error => {
console.info('');
logger.error(error);
});
});

View File

@@ -5,7 +5,7 @@ const stringPadding = require('string-padding');
const cliUtils = {};
cliUtils.printArray = function(logFunction, rows, headers = null) {
cliUtils.printArray = function(logFunction, rows) {
if (!rows.length) return '';
const ALIGN_LEFT = 0;
@@ -16,7 +16,7 @@ cliUtils.printArray = function(logFunction, rows, headers = null) {
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
for (let j = 0; j < row.length; j++) {
let item = row[j];
let width = item ? item.toString().length : 0;
@@ -26,7 +26,6 @@ cliUtils.printArray = function(logFunction, rows, headers = null) {
}
}
let lines = [];
for (let row = 0; row < rows.length; row++) {
let line = [];
for (let col = 0; col < colWidths.length; col++) {
@@ -37,7 +36,7 @@ cliUtils.printArray = function(logFunction, rows, headers = null) {
}
logFunction(line.join(' '));
}
}
};
cliUtils.parseFlags = function(flags) {
let output = {};
@@ -56,10 +55,10 @@ cliUtils.parseFlags = function(flags) {
}
}
return output;
}
};
cliUtils.parseCommandArg = function(arg) {
if (arg.length <= 2) throw new Error('Invalid command arg: ' + arg);
if (arg.length <= 2) throw new Error(`Invalid command arg: ${arg}`);
const c1 = arg[0];
const c2 = arg[arg.length - 1];
@@ -70,9 +69,9 @@ cliUtils.parseCommandArg = function(arg) {
} else if (c1 == '[' && c2 == ']') {
return { required: false, name: name };
} else {
throw new Error('Invalid command arg: ' + arg);
throw new Error(`Invalid command arg: ${arg}`);
}
}
};
cliUtils.makeCommandArgs = function(cmd, argv) {
let cmdUsage = cmd.usage();
@@ -83,9 +82,8 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
let booleanFlags = [];
let aliases = {};
for (let i = 0; i < options.length; i++) {
if (options[i].length != 2) throw new Error('Invalid options: ' + options[i]);
if (options[i].length != 2) throw new Error(`Invalid options: ${options[i]}`);
let flags = options[i][0];
let text = options[i][1];
flags = cliUtils.parseFlags(flags);
@@ -125,27 +123,27 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
output.options = argOptions;
return output;
}
};
cliUtils.promptMcq = function(message, answers) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
message += "\n\n";
message += '\n\n';
for (let n in answers) {
if (!answers.hasOwnProperty(n)) continue;
message += _('%s: %s', n, answers[n]) + "\n";
message += `${_('%s: %s', n, answers[n])}\n`;
}
message += "\n";
message += '\n';
message += _('Your choice: ');
return new Promise((resolve, reject) => {
rl.question(message, (answer) => {
rl.question(message, answer => {
rl.close();
if (!(answer in answers)) {
@@ -156,7 +154,7 @@ cliUtils.promptMcq = function(message, answers) {
resolve(answer);
});
});
}
};
cliUtils.promptConfirm = function(message, answers = null) {
if (!answers) answers = [_('Y'), _('n')];
@@ -164,23 +162,24 @@ cliUtils.promptConfirm = function(message, answers = null) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
message += ' (' + answers.join('/') + ')';
message += ` (${answers.join('/')})`;
return new Promise((resolve, reject) => {
rl.question(message + ' ', (answer) => {
return new Promise((resolve) => {
rl.question(`${message} `, answer => {
const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase();
rl.close();
resolve(ok);
});
});
}
};
// Note: initialText is there to have the same signature as statusBar.prompt() so that
// it can be a drop-in replacement, however initialText is not used (and cannot be
// with readline.question?).
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
if (!options) options = {};
@@ -189,10 +188,9 @@ cliUtils.prompt = function(initialText = '', promptString = ':', options = null)
const mutableStdout = new Writable({
write: function(chunk, encoding, callback) {
if (!this.muted)
process.stdout.write(chunk, encoding);
if (!this.muted) process.stdout.write(chunk, encoding);
callback();
}
},
});
const rl = readline.createInterface({
@@ -201,18 +199,18 @@ cliUtils.prompt = function(initialText = '', promptString = ':', options = null)
terminal: true,
});
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
mutableStdout.muted = false;
rl.question(promptString, (answer) => {
rl.question(promptString, answer => {
rl.close();
if (!!options.secure) this.stdout_('');
if (options.secure) this.stdout_('');
resolve(answer);
});
mutableStdout.muted = !!options.secure;
});
}
};
let redrawStarted_ = false;
let redrawLastLog_ = null;
@@ -220,7 +218,7 @@ let redrawLastUpdateTime_ = 0;
cliUtils.setStdout = function(v) {
this.stdout_ = v;
}
};
cliUtils.redraw = function(s) {
const now = time.unixMs();
@@ -233,8 +231,8 @@ cliUtils.redraw = function(s) {
redrawLastLog_ = s;
}
redrawStarted_ = true;
}
redrawStarted_ = true;
};
cliUtils.redrawDone = function() {
if (!redrawStarted_) return;
@@ -245,6 +243,6 @@ cliUtils.redrawDone = function() {
redrawLastLog_ = null;
redrawStarted_ = false;
}
};
module.exports = { cliUtils };
module.exports = { cliUtils };

View File

@@ -1,19 +1,12 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem');
const BaseModel = require('lib/BaseModel');
const Setting = require('lib/models/Setting.js');
const { toTitleCase } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const markdownUtils = require('lib/markdownUtils');
const { Database } = require('lib/database.js');
class Command extends BaseCommand {
usage() {
return 'apidoc';
}
@@ -23,18 +16,22 @@ class Command extends BaseCommand {
}
createPropertiesTable(tableFields) {
const headers = [
{ name: 'name', label: 'Name' },
{ name: 'type', label: 'Type', filter: (value) => {
return Database.enumName('fieldType', value);
}},
{ name: 'description', label: 'Description' },
];
return markdownUtils.createMarkdownTable(headers, tableFields);
const headers = [
{ name: 'name', label: 'Name' },
{
name: 'type',
label: 'Type',
filter: value => {
return Database.enumName('fieldType', value);
},
},
{ name: 'description', label: 'Description' },
];
return markdownUtils.createMarkdownTable(headers, tableFields);
}
async action(args) {
async action() {
const models = [
{
type: BaseModel.TYPE_NOTE,
@@ -70,8 +67,8 @@ class Command extends BaseCommand {
lines.push('}');
lines.push('```');
lines.push('');
lines.push('# Authorisation')
lines.push('# Authorisation');
lines.push('');
lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.');
lines.push('');
@@ -171,7 +168,7 @@ class Command extends BaseCommand {
// });
}
lines.push('# ' + toTitleCase(tableName));
lines.push(`# ${toTitleCase(tableName)}`);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
@@ -184,9 +181,9 @@ class Command extends BaseCommand {
lines.push(this.createPropertiesTable(tableFields));
lines.push('');
lines.push('## GET /' + tableName);
lines.push(`## GET /${tableName}`);
lines.push('');
lines.push('Gets all ' + tableName);
lines.push(`Gets all ${tableName}`);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
@@ -194,9 +191,9 @@ class Command extends BaseCommand {
lines.push('');
}
lines.push('## GET /' + tableName + '/:id');
lines.push(`## GET /${tableName}/:id`);
lines.push('');
lines.push('Gets ' + singular + ' with ID :id');
lines.push(`Gets ${singular} with ID :id`);
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
@@ -227,9 +224,9 @@ class Command extends BaseCommand {
lines.push('');
}
lines.push('## POST /' + tableName);
lines.push(`## POST /${tableName}`);
lines.push('');
lines.push('Creates a new ' + singular);
lines.push(`Creates a new ${singular}`);
lines.push('');
if (model.type === BaseModel.TYPE_RESOURCE) {
@@ -273,14 +270,14 @@ class Command extends BaseCommand {
lines.push('');
}
lines.push('## PUT /' + tableName + '/:id');
lines.push(`## PUT /${tableName}/:id`);
lines.push('');
lines.push('Sets the properties of the ' + singular + ' with ID :id');
lines.push(`Sets the properties of the ${singular} with ID :id`);
lines.push('');
lines.push('## DELETE /' + tableName + '/:id');
lines.push(`## DELETE /${tableName}/:id`);
lines.push('');
lines.push('Deletes the ' + singular + ' with ID :id');
lines.push(`Deletes the ${singular} with ID :id`);
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
@@ -293,7 +290,6 @@ class Command extends BaseCommand {
this.stdout(lines.join('\n'));
}
}
module.exports = Command;

View File

@@ -3,10 +3,8 @@ const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'attach <note> <file>';
}
@@ -26,7 +24,6 @@ class Command extends BaseCommand {
await shim.attachFileToNote(note, localFilePath);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'cat <note>';
}
@@ -16,9 +14,7 @@ class Command extends BaseCommand {
}
options() {
return [
['-v, --verbose', _('Displays the complete information about note.')],
];
return [['-v, --verbose', _('Displays the complete information about note.')]];
}
async action(args) {
@@ -30,10 +26,13 @@ class Command extends BaseCommand {
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
this.stdout(content);
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -4,25 +4,22 @@ const { app } = require('./app.js');
const Setting = require('lib/models/Setting.js');
class Command extends BaseCommand {
usage() {
return 'config [name] [value]';
}
description() {
return _("Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.");
return _('Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.');
}
options() {
return [
['-v, --verbose', _('Also displays unset and hidden config variables.')],
];
return [['-v, --verbose', _('Also displays unset and hidden config variables.')]];
}
async action(args) {
const verbose = args.options.verbose;
const renderKeyValue = (name) => {
const renderKeyValue = name => {
const md = Setting.settingMetadata(name);
let value = Setting.value(name);
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
@@ -33,7 +30,7 @@ class Command extends BaseCommand {
} else {
return _('%s = %s', name, value);
}
}
};
if (!args.name && !args.value) {
let keys = Setting.keys(!verbose, 'cli');
@@ -43,15 +40,23 @@ class Command extends BaseCommand {
if (!verbose && !value) continue;
this.stdout(renderKeyValue(keys[i]));
}
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
return;
}
if (args.name && !args.value) {
this.stdout(renderKeyValue(args.name));
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
return;
}
@@ -64,7 +69,6 @@ class Command extends BaseCommand {
await Setting.saveAll();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'cp <note> [notebook]';
}
@@ -33,7 +31,6 @@ class Command extends BaseCommand {
Note.updateGeolocation(newNote.id);
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,12 +2,10 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'done <note>';
}
@@ -35,7 +33,6 @@ class Command extends BaseCommand {
async action(args) {
await Command.handleAction(this, args, true);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,12 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
class Command extends BaseCommand {
usage() {
return 'dump';
}
@@ -19,7 +16,7 @@ class Command extends BaseCommand {
return true;
}
async action(args) {
async action() {
let items = [];
let folders = await Folder.all();
for (let i = 0; i < folders.length; i++) {
@@ -35,10 +32,9 @@ class Command extends BaseCommand {
}
items = items.concat(tags);
this.stdout(JSON.stringify(items));
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,9 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem');
const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim');
@@ -12,7 +10,6 @@ const imageType = require('image-type');
const readChunk = require('read-chunk');
class Command extends BaseCommand {
usage() {
return 'e2ee <command> [path]';
}
@@ -35,7 +32,7 @@ class Command extends BaseCommand {
const options = args.options;
const askForMasterKey = async (error) => {
const askForMasterKey = async error => {
const masterKeyId = error.masterKeyId;
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
if (!password) {
@@ -45,7 +42,7 @@ class Command extends BaseCommand {
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
await EncryptionService.instance().loadMasterKeysFromSettings();
return true;
}
};
if (args.command === 'enable') {
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
@@ -100,13 +97,13 @@ class Command extends BaseCommand {
while (true) {
try {
const outputDir = options.output ? options.output : require('os').tmpdir();
let outFile = outputDir + '/' + pathUtils.filename(args.path) + '.' + Date.now() + '.bin';
let outFile = `${outputDir}/${pathUtils.filename(args.path)}.${Date.now()}.bin`;
await EncryptionService.instance().decryptFile(args.path, outFile);
const buffer = await readChunk(outFile, 0, 64);
const detectedType = imageType(buffer);
if (detectedType) {
const newOutFile = outFile + '.' + detectedType.ext;
const newOutFile = `${outFile}.${detectedType.ext}`;
await shim.fsDriver().move(outFile, newOutFile);
outFile = newOutFile;
}
@@ -128,19 +125,17 @@ class Command extends BaseCommand {
if (args.command === 'target-status') {
const fs = require('fs-extra');
const pathUtils = require('lib/path-utils.js');
const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)();
const targetPath = args.path;
if (!targetPath) throw new Error('Please specify the sync target path.');
const dirPaths = function(targetPath) {
let paths = [];
fs.readdirSync(targetPath).forEach((path) => {
fs.readdirSync(targetPath).forEach(path => {
paths.push(path);
});
return paths;
}
};
let itemCount = 0;
let resourceCount = 0;
@@ -155,7 +150,7 @@ class Command extends BaseCommand {
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const fullPath = targetPath + '/' + path;
const fullPath = `${targetPath}/${path}`;
const stat = await fs.stat(fullPath);
// this.stdout(fullPath);
@@ -165,7 +160,7 @@ class Command extends BaseCommand {
for (let j = 0; j < resourcePaths.length; j++) {
const resourcePath = resourcePaths[j];
resourceCount++;
const fullResourcePath = fullPath + '/' + resourcePath;
const fullResourcePath = `${fullPath}/${resourcePath}`;
const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath);
if (isEncrypted) {
encryptedResourceCount++;
@@ -199,9 +194,9 @@ class Command extends BaseCommand {
}
}
this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount);
this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount);
this.stdout('Other items (never encrypted): ' + otherItemCount);
this.stdout(`Encrypted items: ${encryptedItemCount}/${itemCount}`);
this.stdout(`Encrypted resources: ${encryptedResourceCount}/${resourceCount}`);
this.stdout(`Other items (never encrypted): ${otherItemCount}`);
if (options.verbose) {
this.stdout('');
@@ -224,7 +219,6 @@ class Command extends BaseCommand {
return;
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -3,15 +3,11 @@ const { BaseCommand } = require('./base-command.js');
const { uuid } = require('lib/uuid.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'edit <note>';
}
@@ -21,20 +17,19 @@ class Command extends BaseCommand {
}
async action(args) {
let watcher = null;
let tempFilePath = null;
const onFinishedEditing = async () => {
if (tempFilePath) fs.removeSync(tempFilePath);
}
};
const textEditorPath = () => {
if (Setting.value('editor')) return Setting.value('editor');
if (process.env.EDITOR) return process.env.EDITOR;
throw new Error(_('No text editor is defined. Please set it using `config editor <editor-path>`'));
}
};
try {
try {
// -------------------------------------------------------------------------
// Load note or create it if it doesn't exist
// -------------------------------------------------------------------------
@@ -65,7 +60,7 @@ class Command extends BaseCommand {
const originalContent = await Note.serializeForEdit(note);
tempFilePath = Setting.value('tempDir') + '/' + uuid.create() + '.md';
tempFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`;
editorArgs.push(tempFilePath);
await fs.writeFile(tempFilePath, originalContent);
@@ -76,18 +71,30 @@ class Command extends BaseCommand {
this.logger().info('Disabling fullscreen...');
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
await app().gui().forceRender();
const termState = app().gui().termSaveState();
app()
.gui()
.showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
await app()
.gui()
.forceRender();
const termState = app()
.gui()
.termSaveState();
const spawnSync = require('child_process').spawnSync;
const spawnSync = require('child_process').spawnSync;
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
app().gui().termRestoreState(termState);
app().gui().hideModalOverlay();
app().gui().forceRender();
app()
.gui()
.termRestoreState(termState);
app()
.gui()
.hideModalOverlay();
app()
.gui()
.forceRender();
// -------------------------------------------------------------------------
// Save the note and clean up
@@ -107,13 +114,11 @@ class Command extends BaseCommand {
});
await onFinishedEditing();
} catch(error) {
} catch (error) {
await onFinishedEditing();
throw error;
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -3,7 +3,6 @@ const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {
usage() {
return 'exit';
}
@@ -16,10 +15,9 @@ class Command extends BaseCommand {
return ['gui'];
}
async action(args) {
async action() {
await app().exit();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,13 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export-sync-status';
}
@@ -20,17 +17,20 @@ class Command extends BaseCommand {
return true;
}
async action(args) {
async action() {
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv';
const filePath = `${Setting.value('profileDir')}/syncReport-${new Date().getTime()}.csv`;
await fs.writeFileSync(filePath, csv);
this.stdout('Sync status exported to ' + filePath);
this.stdout(`Sync status exported to ${filePath}`);
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,14 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const InteropService = require('lib/services/InteropService.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export <path>';
}
@@ -19,17 +15,14 @@ class Command extends BaseCommand {
options() {
const service = new InteropService();
const formats = service.modules()
const formats = service
.modules()
.filter(m => m.type === 'exporter')
.map(m => m.format + (m.description ? ' (' + m.description + ')' : ''));
.map(m => m.format + (m.description ? ` (${m.description})` : ''));
return [
['--format <format>', _('Destination format: %s', formats.join(', '))],
['--note <note>', _('Exports only the given note.')],
['--notebook <notebook>', _('Exports only the given notebook.')],
];
return [['--format <format>', _('Destination format: %s', formats.join(', '))], ['--note <note>', _('Exports only the given note.')], ['--notebook <notebook>', _('Exports only the given notebook.')]];
}
async action(args) {
let exportOptions = {};
exportOptions.path = args.path;
@@ -37,25 +30,20 @@ class Command extends BaseCommand {
exportOptions.format = args.options.format ? args.options.format : 'jex';
if (args.options.note) {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() });
if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note));
exportOptions.sourceNoteIds = notes.map((n) => n.id);
exportOptions.sourceNoteIds = notes.map(n => n.id);
} else if (args.options.notebook) {
const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook);
if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook));
exportOptions.sourceFolderIds = folders.map((n) => n.id);
exportOptions.sourceFolderIds = folders.map(n => n.id);
}
const service = new InteropService();
const result = await service.export(exportOptions);
result.warnings.map((w) => this.stdout(w));
result.warnings.map(w => this.stdout(w));
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'geoloc <note>';
}
@@ -23,9 +21,10 @@ class Command extends BaseCommand {
const url = Note.geolocationUrl(item);
this.stdout(url);
app().gui().showConsole();
app()
.gui()
.showConsole();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,14 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { renderCommandHelp } = require('./help-utils.js');
const { Database } = require('lib/database.js');
const Setting = require('lib/models/Setting.js');
const { wrap } = require('lib/string-utils.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'help [command]';
}
@@ -28,7 +24,7 @@ class Command extends BaseCommand {
output.push(command);
}
output.sort((a, b) => a.name() < b.name() ? -1 : +1);
output.sort((a, b) => (a.name() < b.name() ? -1 : +1));
return output;
}
@@ -40,31 +36,37 @@ class Command extends BaseCommand {
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplinapp.org/terminal/#shortcuts'));
this.stdout('');
if (app().gui().isDummy()) {
if (
app()
.gui()
.isDummy()
) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const keymap = app().gui().keymap();
const keymap = app()
.gui()
.keymap();
let rows = [];
for (let i = 0; i < keymap.length; i++) {
const item = keymap[i];
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
const keys = item.keys.map(k => (k === ' ' ? '(SPACE)' : k));
rows.push([keys.join(', '), item.command]);
}
cliUtils.printArray(this.stdout.bind(this), rows);
} else if (args.command === 'all') {
const commands = this.allCommands();
const output = commands.map((c) => renderCommandHelp(c));
const output = commands.map(c => renderCommandHelp(c));
this.stdout(output.join('\n\n'));
} else if (args.command) {
const command = app().findCommandByName(args['command']);
if (!command) throw new Error(_('Cannot find "%s".', args.command));
this.stdout(renderCommandHelp(command, stdoutWidth));
} else {
const commandNames = this.allCommands().map((a) => a.name());
const commandNames = this.allCommands().map(a => a.name());
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
this.stdout('');
@@ -82,10 +84,13 @@ class Command extends BaseCommand {
this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`'));
}
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,17 +1,11 @@
const { BaseCommand } = require('./base-command.js');
const InteropService = require('lib/services/InteropService.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const { filename, basename, fileExtension } = require('lib/path-utils.js');
const { importEnex } = require('lib/import-enex');
const { cliUtils } = require('./cli-utils.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'import <path> [notebook]';
}
@@ -22,14 +16,14 @@ class Command extends BaseCommand {
options() {
const service = new InteropService();
const formats = service.modules().filter(m => m.type === 'importer').map(m => m.format);
const formats = service
.modules()
.filter(m => m.type === 'importer')
.map(m => m.format);
return [
['--format <format>', _('Source format: %s', (['auto'].concat(formats)).join(', '))],
['-f, --force', _('Do not ask for confirmation.')],
];
return [['--format <format>', _('Source format: %s', ['auto'].concat(formats).join(', '))], ['-f, --force', _('Do not ask for confirmation.')]];
}
async action(args) {
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
@@ -44,7 +38,7 @@ class Command extends BaseCommand {
// onProgress/onError supported by Enex import only
importOptions.onProgress = (progressState) => {
importOptions.onProgress = progressState => {
let line = [];
line.push(_('Found: %d.', progressState.loaded));
line.push(_('Created: %d.', progressState.created));
@@ -56,20 +50,21 @@ class Command extends BaseCommand {
cliUtils.redraw(lastProgress);
};
importOptions.onError = (error) => {
importOptions.onError = error => {
let s = error.trace ? error.trace : error.toString();
this.stdout(s);
};
app().gui().showConsole();
app()
.gui()
.showConsole();
this.stdout(_('Importing notes...'));
const service = new InteropService();
const result = await service.import(importOptions);
result.warnings.map((w) => this.stdout(w));
result.warnings.map(w => this.stdout(w));
cliUtils.redrawDone();
if (lastProgress) this.stdout(_('The notes have been imported: %s', lastProgress));
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -10,7 +10,6 @@ const { time } = require('lib/time-utils.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'ls [note-pattern]';
}
@@ -24,14 +23,7 @@ class Command extends BaseCommand {
}
options() {
return [
['-n, --limit <num>', _('Displays only the first top <num> notes.')],
['-s, --sort <field>', _('Sorts the item by <field> (eg. title, updated_time, created_time).')],
['-r, --reverse', _('Reverses the sorting order.')],
['-t, --type <type>', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')],
['-f, --format <format>', _('Either "text" or "json"')],
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
return [['-n, --limit <num>', _('Displays only the first top <num> notes.')], ['-s, --sort <field>', _('Sorts the item by <field> (eg. title, updated_time, created_time).')], ['-r, --reverse', _('Reverses the sorting order.')], ['-t, --type <type>', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')], ['-f, --format <format>', _('Either "text" or "json"')], ['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')]];
}
async action(args) {
@@ -98,14 +90,14 @@ class Command extends BaseCommand {
let title = item.title;
if (!shortIdShown && (seenTitles.indexOf(item.title) >= 0 || !item.title)) {
title += ' (' + BaseModel.shortId(item.id) + ')';
title += ` (${BaseModel.shortId(item.id)})`;
} else {
seenTitles.push(item.title);
}
if (hasTodos) {
if (item.is_todo) {
row.push(sprintf('[%s]', !!item.todo_completed ? 'X' : ' '));
row.push(sprintf('[%s]', item.todo_completed ? 'X' : ' '));
} else {
row.push(' ');
}
@@ -118,9 +110,7 @@ class Command extends BaseCommand {
cliUtils.printArray(this.stdout.bind(this), rows);
}
}
}
module.exports = Command;

View File

@@ -2,10 +2,8 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const { reg } = require('lib/registry.js');
class Command extends BaseCommand {
usage() {
return 'mkbook <new-notebook>';
}
@@ -15,10 +13,9 @@ class Command extends BaseCommand {
}
async action(args) {
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
app().switchCurrentFolder(folder);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -4,7 +4,6 @@ const { _ } = require('lib/locale.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mknote <new-note>';
}
@@ -26,7 +25,6 @@ class Command extends BaseCommand {
app().switchCurrentFolder(app().currentFolder());
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -4,7 +4,6 @@ const { _ } = require('lib/locale.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mktodo <new-todo>';
}
@@ -27,7 +26,6 @@ class Command extends BaseCommand {
app().switchCurrentFolder(app().currentFolder());
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -6,7 +6,6 @@ const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mv <note> [notebook]';
}
@@ -18,7 +17,7 @@ class Command extends BaseCommand {
async action(args) {
const pattern = args['note'];
const destination = args['notebook'];
const folder = await Folder.loadByField('title', destination);
if (!folder) throw new Error(_('Cannot find "%s".', destination));
@@ -29,7 +28,6 @@ class Command extends BaseCommand {
await Note.moveToFolder(notes[i].id, folder.id);
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -6,7 +6,6 @@ const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'ren <item> <name>';
}
@@ -35,7 +34,6 @@ class Command extends BaseCommand {
await Note.save(newItem);
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,14 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'rmbook <notebook>';
}
@@ -18,9 +14,7 @@ class Command extends BaseCommand {
}
options() {
return [
['-f, --force', _('Deletes the notebook without asking for confirmation.')],
];
return [['-f, --force', _('Deletes the notebook without asking for confirmation.')]];
}
async action(args) {
@@ -34,7 +28,6 @@ class Command extends BaseCommand {
await Folder.delete(folder.id);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,14 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'rmnote <note-pattern>';
}
@@ -18,9 +14,7 @@ class Command extends BaseCommand {
}
options() {
return [
['-f, --force', _('Deletes the notes without asking for confirmation.')],
];
return [['-f, --force', _('Deletes the notes without asking for confirmation.')]];
}
async action(args) {
@@ -32,10 +26,9 @@ class Command extends BaseCommand {
const ok = force ? true : await this.prompt(notes.length > 1 ? _('%d notes match this pattern. Delete them?', notes.length) : _('Delete note?'), { booleanAnswerDefault: 'n' });
if (!ok) return;
let ids = notes.map((n) => n.id);
let ids = notes.map(n => n.id);
await Note.batchDelete(ids);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,15 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { uuid } = require('lib/uuid.js');
class Command extends BaseCommand {
usage() {
return 'search <pattern> [notebook]';
}
@@ -50,7 +45,6 @@ class Command extends BaseCommand {
id: searchId,
});
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -0,0 +1,57 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
class Command extends BaseCommand {
usage() {
return 'server <command>';
}
description() {
return `${_('Start, stop or check the API server. To specify on which port it should run, set the api.port config variable. Commands are (%s).', ['start', 'stop', 'status'].join('|'))} This is an experimental feature - use at your own risks! It is recommended that the server runs off its own separate profile so that no two CLI instances access that profile at the same time. Use --profile to specify the profile path.`;
}
async action(args) {
const command = args.command;
const ClipperServer = require('lib/ClipperServer');
const stdoutFn = (s) => this.stdout(s);
const clipperLogger = new Logger();
clipperLogger.addTarget('file', { path: `${Setting.value('profileDir')}/log-clipper.txt` });
clipperLogger.addTarget('console', { console: {
info: stdoutFn,
warn: stdoutFn,
error: stdoutFn,
}});
ClipperServer.instance().setDispatch(() => {});
ClipperServer.instance().setLogger(clipperLogger);
const pidPath = `${Setting.value('profileDir')}/clipper-pid.txt`;
const runningOnPort = await ClipperServer.instance().isRunning();
if (command === 'start') {
if (runningOnPort) {
this.stdout(_('Server is already running on port %d', runningOnPort));
} else {
await shim.fsDriver().writeFile(pidPath, process.pid.toString(), 'utf-8');
await ClipperServer.instance().start(); // Never exit
}
} else if (command === 'status') {
this.stdout(runningOnPort ? _('Server is running on port %d', runningOnPort) : _('Server is not running.'));
} else if (command === 'stop') {
if (!runningOnPort) {
this.stdout(_('Server is not running.'));
return;
}
const pid = await shim.fsDriver().readFile(pidPath);
if (!pid) return;
process.kill(pid, 'SIGTERM');
}
}
}
module.exports = Command;

View File

@@ -3,12 +3,9 @@ const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const { Database } = require('lib/database.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
class Command extends BaseCommand {
usage() {
return 'set <note> <name> [value]';
}
@@ -19,7 +16,7 @@ class Command extends BaseCommand {
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
if (f.name === 'id') continue;
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
s.push(`${f.name} (${Database.enumName('fieldType', f.type)})`);
}
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
@@ -45,7 +42,6 @@ class Command extends BaseCommand {
await Note.save(newNote);
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,12 +1,10 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
class Command extends BaseCommand {
usage() {
return 'status';
}
@@ -15,7 +13,7 @@ class Command extends BaseCommand {
return _('Displays summary about the notes and notebooks.');
}
async action(args) {
async action() {
let service = new ReportService();
let report = await service.status(Setting.value('sync.target'));
@@ -24,7 +22,7 @@ class Command extends BaseCommand {
if (i > 0) this.stdout('');
this.stdout('# ' + section.title);
this.stdout(`# ${section.title}`);
this.stdout('');
for (let n in section.body) {
@@ -34,10 +32,13 @@ class Command extends BaseCommand {
}
}
app().gui().showConsole();
app().gui().maximizeConsole();
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -3,7 +3,6 @@ const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
const Setting = require('lib/models/Setting.js');
const BaseItem = require('lib/models/BaseItem.js');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const { Synchronizer } = require('lib/synchronizer.js');
const { reg } = require('lib/registry.js');
@@ -14,7 +13,6 @@ const fs = require('fs-extra');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class Command extends BaseCommand {
constructor() {
super();
this.syncTargetId_ = null;
@@ -31,9 +29,7 @@ class Command extends BaseCommand {
}
options() {
return [
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
];
return [['--target <target>', _('Sync to provided target (defaults to sync.target config value)')]];
}
static lockFile(filePath) {
@@ -66,21 +62,25 @@ class Command extends BaseCommand {
const syncTarget = reg.syncTarget(this.syncTargetId_);
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
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); }
log: (...s) => {
return this.stdout(...s);
},
});
this.oneDriveApiUtils_ = null;
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : 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;
} else if (syncTargetMd.name === 'dropbox') { // Dropbox
} else if (syncTargetMd.name === 'dropbox') {
// Dropbox
const api = await syncTarget.api();
const loginUrl = api.loginUrl();
this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
@@ -93,7 +93,7 @@ class Command extends BaseCommand {
}
const response = await api.execAuthToken(authCode);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
Setting.setValue(`sync.${this.syncTargetId_}.auth`, response.access_token);
api.setAuthToken(response.access_token);
return true;
}
@@ -117,8 +117,8 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
const lockFilePath = `${require('os').tmpdir()}/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 {
if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.'));
@@ -147,22 +147,26 @@ class Command extends BaseCommand {
const syncTarget = reg.syncTarget(this.syncTargetId_);
if (!await syncTarget.isAuthenticated()) {
app().gui().showConsole();
app().gui().maximizeConsole();
if (!(await syncTarget.isAuthenticated())) {
app()
.gui()
.showConsole();
app()
.gui()
.maximizeConsole();
const authDone = await this.doAuth();
if (!authDone) return cleanUp();
}
const sync = await syncTarget.synchronizer();
let options = {
onProgress: (report) => {
onProgress: report => {
let lines = Synchronizer.reportToLines(report);
if (lines.length) cliUtils.redraw(lines.join(' '));
},
onMessage: (msg) => {
onMessage: msg => {
cliUtils.redrawDone();
this.stdout(msg);
},
@@ -174,7 +178,7 @@ class Command extends BaseCommand {
this.stdout(_('Starting synchronisation...'));
const contextKey = 'sync.' + this.syncTargetId_ + '.context';
const contextKey = `sync.${this.syncTargetId_}.context`;
let context = Setting.value(contextKey);
context = context ? JSON.parse(context) : {};
@@ -237,7 +241,6 @@ class Command extends BaseCommand {
cancellable() {
return true;
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -6,7 +6,6 @@ const BaseModel = require('lib/BaseModel.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'tag <tag-command> [tag] [note]';
}
@@ -16,9 +15,7 @@ class Command extends BaseCommand {
}
options() {
return [
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
return [['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')]];
}
async action(args) {
@@ -50,7 +47,7 @@ class Command extends BaseCommand {
} else if (command == 'list') {
if (tag) {
let notes = await Tag.notes(tag.id);
notes.map((note) => {
notes.map(note => {
let line = '';
if (options.long) {
line += BaseModel.shortId(note.id);
@@ -61,7 +58,7 @@ class Command extends BaseCommand {
if (note.is_todo) {
line += '[';
if (note.todo_completed) {
line += 'X';
line += 'X';
} else {
line += ' ';
}
@@ -74,13 +71,14 @@ class Command extends BaseCommand {
});
} else {
let tags = await Tag.all();
tags.map((tag) => { this.stdout(tag.title); });
tags.map(tag => {
this.stdout(tag.title);
});
}
} else {
throw new Error(_('Invalid command: "%s"', command));
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,12 +2,10 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'todo <todo-command> <note-pattern>';
}
@@ -39,12 +37,11 @@ class Command extends BaseCommand {
}
} else if (action == 'clear') {
toSave.is_todo = 0;
}
}
await Note.save(toSave);
}
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -1,15 +1,9 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
const CommandDone = require('./command-done.js');
class Command extends BaseCommand {
usage() {
return 'undone <note>';
}
@@ -21,7 +15,6 @@ class Command extends BaseCommand {
async action(args) {
await CommandDone.handleAction(this, args, false);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -2,10 +2,8 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
class Command extends BaseCommand {
usage() {
return 'use <notebook>';
}
@@ -14,10 +12,6 @@ class Command extends BaseCommand {
return _('Switches to [notebook] - all further operations will happen within this notebook.');
}
autocomplete() {
return { data: autocompleteFolders };
}
compatibleUis() {
return ['cli'];
}
@@ -27,7 +21,6 @@ class Command extends BaseCommand {
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
app().switchCurrentFolder(folder);
}
}
module.exports = Command;
module.exports = Command;

View File

@@ -3,7 +3,6 @@ const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {
usage() {
return 'version';
}
@@ -12,11 +11,10 @@ class Command extends BaseCommand {
return _('Displays version information');
}
async action(args) {
async action() {
const p = require('./package.json');
this.stdout(_('%s %s (%s)', p.name, p.version, Setting.value('env')));
}
}
module.exports = Command;
module.exports = Command;

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,6 @@
const TextWidget = require('tkwidgets/TextWidget.js');
class ConsoleWidget extends TextWidget {
constructor() {
super();
this.lines_ = [];
@@ -16,7 +15,7 @@ class ConsoleWidget extends TextWidget {
}
get lastLine() {
return this.lines_.length ? this.lines_[this.lines_.length-1] : '';
return this.lines_.length ? this.lines_[this.lines_.length - 1] : '';
}
addLine(line) {
@@ -40,13 +39,12 @@ class ConsoleWidget extends TextWidget {
if (this.lines_.length > this.maxLines_) {
this.lines_.splice(0, this.lines_.length - this.maxLines_);
}
this.text = this.lines_.join("\n");
this.text = this.lines_.join('\n');
this.updateText_ = false;
}
super.render();
}
}
module.exports = ConsoleWidget;
module.exports = ConsoleWidget;

View File

@@ -5,7 +5,6 @@ const ListWidget = require('tkwidgets/ListWidget.js');
const _ = require('lib/locale.js')._;
class FolderListWidget extends ListWidget {
constructor() {
super();
@@ -20,19 +19,19 @@ class FolderListWidget extends ListWidget {
this.updateItems_ = false;
this.trimItemTitle = false;
this.itemRenderer = (item) => {
this.itemRenderer = item => {
let output = [];
if (item === '-') {
output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) {
output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
} else if (item.type_ === Tag.modelType()) {
output.push('[' + Folder.displayTitle(item) + ']');
output.push(`[${Folder.displayTitle(item)}]`);
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:'));
output.push(item.title);
}
return output.join(' ');
};
}
@@ -45,7 +44,6 @@ class FolderListWidget extends ListWidget {
output++;
folderId = folder.parent_id;
}
throw new Error('unreachable');
}
get selectedFolderId() {
@@ -54,7 +52,7 @@ class FolderListWidget extends ListWidget {
set selectedFolderId(v) {
this.selectedFolderId_ = v;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -64,7 +62,7 @@ class FolderListWidget extends ListWidget {
set selectedSearchId(v) {
this.selectedSearchId_ = v;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -74,7 +72,7 @@ class FolderListWidget extends ListWidget {
set selectedTagId(v) {
this.selectedTagId_ = v;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -84,7 +82,7 @@ class FolderListWidget extends ListWidget {
set notesParentType(v) {
this.notesParentType_ = v;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -95,7 +93,7 @@ class FolderListWidget extends ListWidget {
set searches(v) {
this.searches_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -106,7 +104,7 @@ class FolderListWidget extends ListWidget {
set tags(v) {
this.tags_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -117,7 +115,7 @@ class FolderListWidget extends ListWidget {
set folders(v) {
this.folders_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.updateIndexFromSelectedItemId();
this.invalidate();
}
@@ -128,7 +126,7 @@ class FolderListWidget extends ListWidget {
}
return false;
}
render() {
if (this.updateItems_) {
this.logger().debug('Rebuilding items...', this.notesParentType, this.selectedJoplinItemId, this.selectedSearchId);
@@ -136,7 +134,7 @@ class FolderListWidget extends ListWidget {
const previousParentType = this.notesParentType;
let newItems = [];
const orderFolders = (parentId) => {
const orderFolders = parentId => {
for (let i = 0; i < this.folders.length; i++) {
const f = this.folders[i];
const folderParentId = f.parent_id ? f.parent_id : '';
@@ -145,7 +143,7 @@ class FolderListWidget extends ListWidget {
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
}
}
}
};
orderFolders('');
@@ -162,7 +160,7 @@ class FolderListWidget extends ListWidget {
this.items = newItems;
this.notesParentType = previousParentType;
this.updateIndexFromSelectedItemId(wasSelectedItemId)
this.updateIndexFromSelectedItemId(wasSelectedItemId);
this.updateItems_ = false;
}
@@ -174,7 +172,7 @@ class FolderListWidget extends ListWidget {
if (this.notesParentType === 'Folder') return this.selectedFolderId;
if (this.notesParentType === 'Tag') return this.selectedTagId;
if (this.notesParentType === 'Search') return this.selectedSearchId;
throw new Error('Unknown parent type: ' + this.notesParentType);
throw new Error(`Unknown parent type: ${this.notesParentType}`);
}
get selectedJoplinItem() {
@@ -188,7 +186,6 @@ class FolderListWidget extends ListWidget {
const index = this.itemIndexByKey('id', itemId);
this.currentIndex = index >= 0 ? index : 0;
}
}
module.exports = FolderListWidget;
module.exports = FolderListWidget;

View File

@@ -2,17 +2,16 @@ const Note = require('lib/models/Note.js');
const ListWidget = require('tkwidgets/ListWidget.js');
class NoteListWidget extends ListWidget {
constructor() {
super();
this.selectedNoteId_ = 0;
this.updateIndexFromSelectedNoteId_ = false;
this.itemRenderer = (note) => {
this.itemRenderer = note => {
let label = Note.displayTitle(note); // + ' ' + note.id;
if (note.is_todo) {
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
label = `[${note.todo_completed ? 'X' : ' '}] ${label}`;
}
return label;
};
@@ -32,7 +31,6 @@ class NoteListWidget extends ListWidget {
super.render();
}
}
module.exports = NoteListWidget;
module.exports = NoteListWidget;

View File

@@ -2,7 +2,6 @@ const Note = require('lib/models/Note.js');
const TextWidget = require('tkwidgets/TextWidget.js');
class NoteMetadataWidget extends TextWidget {
constructor() {
super();
this.noteId_ = 0;
@@ -30,7 +29,6 @@ class NoteMetadataWidget extends TextWidget {
this.text = this.note_ ? await Note.minimalSerializeForDisplay(this.note_) : '';
}
}
}
module.exports = NoteMetadataWidget;
module.exports = NoteMetadataWidget;

View File

@@ -3,7 +3,6 @@ const TextWidget = require('tkwidgets/TextWidget.js');
const { _ } = require('lib/locale.js');
class NoteWidget extends TextWidget {
constructor() {
super();
this.noteId_ = 0;
@@ -44,11 +43,11 @@ class NoteWidget extends TextWidget {
} else if (this.noteId_) {
this.doAsync('loadNote', async () => {
this.note_ = await Note.load(this.noteId_);
if (this.note_ && this.note_.encryption_applied) {
this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.');
} else {
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
this.text = this.note_ ? `${this.note_.title}\n\n${this.note_.body}` : '';
}
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
@@ -59,7 +58,6 @@ class NoteWidget extends TextWidget {
this.scrollTop = 0;
}
}
}
module.exports = NoteWidget;
module.exports = NoteWidget;

View File

@@ -5,7 +5,6 @@ const stripAnsi = require('strip-ansi');
const { handleAutocompletion } = require('../autocompletion.js');
class StatusBarWidget extends BaseWidget {
constructor() {
super();
@@ -75,7 +74,7 @@ class StatusBarWidget extends BaseWidget {
super.render();
const doSaveCursor = !this.promptActive;
if (doSaveCursor) this.term.saveCursor();
this.innerClear();
@@ -87,14 +86,13 @@ class StatusBarWidget extends BaseWidget {
//const textStyle = this.promptActive ? (s) => s : chalk.bgBlueBright.white;
//const textStyle = (s) => s;
const textStyle = this.promptActive ? (s) => s : chalk.gray;
const textStyle = this.promptActive ? s => s : chalk.gray;
this.term.drawHLine(this.absoluteInnerX, this.absoluteInnerY, this.innerWidth, textStyle(' '));
this.term.moveTo(this.absoluteInnerX, this.absoluteInnerY);
if (this.promptActive) {
this.term.write(textStyle(this.promptState_.promptString));
if (this.inputEventEmitter_) {
@@ -113,8 +111,8 @@ class StatusBarWidget extends BaseWidget {
history: this.history,
default: this.promptState_.initialText,
autoComplete: handleAutocompletion,
autoCompleteHint : true,
autoCompleteMenu : true,
autoCompleteHint: true,
autoCompleteMenu: true,
};
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
@@ -153,19 +151,15 @@ class StatusBarWidget extends BaseWidget {
// Only callback once everything has been cleaned up and reset
resolveFn(resolveResult);
});
} else {
for (let i = 0; i < this.items_.length; i++) {
const s = this.items_[i].substr(0, this.innerWidth - 1);
this.term.write(textStyle(s));
}
}
if (doSaveCursor) this.term.restoreCursor();
}
}
module.exports = StatusBarWidget;

View File

@@ -1,10 +1,7 @@
const fs = require('fs-extra');
const { wrap } = require('lib/string-utils.js');
const Setting = require('lib/models/Setting.js');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const { _, setLocale, languageCode } = require('lib/locale.js');
const { _ } = require('lib/locale.js');
const rootDir = dirname(dirname(__dirname));
const MAX_WIDTH = 78;
const INDENT = ' ';
@@ -16,14 +13,14 @@ function renderTwoColumnData(options, baseIndent, width) {
let option = options[i];
const flag = option[0];
const indent = baseIndent + INDENT + ' '.repeat(optionColWidth + 2);
let r = wrap(option[1], indent, width);
r = r.substr(flag.length + (baseIndent + INDENT).length);
r = baseIndent + INDENT + flag + r;
output.push(r);
}
return output.join("\n");
return output.join('\n');
}
function renderCommandHelp(cmd, width = null) {
@@ -44,7 +41,7 @@ function renderCommandHelp(cmd, width = null) {
}
if (cmd.name() === 'config') {
const renderMetadata = (md) => {
const renderMetadata = md => {
let desc = [];
if (md.label) {
@@ -63,17 +60,17 @@ function renderCommandHelp(cmd, width = null) {
if ('value' in md) {
if (md.type === Setting.TYPE_STRING) {
defaultString = md.value ? '"' + md.value + '"' : null;
defaultString = md.value ? `"${md.value}"` : null;
} else if (md.type === Setting.TYPE_INT) {
defaultString = (md.value ? md.value : 0).toString();
} else if (md.type === Setting.TYPE_BOOL) {
defaultString = (md.value === true ? 'true' : 'false');
defaultString = md.value === true ? 'true' : 'false';
}
}
if (defaultString !== null) desc.push(_('Default: %s', defaultString));
return [md.key, desc.join("\n")];
return [md.key, desc.join('\n')];
};
output.push('');
@@ -83,7 +80,7 @@ function renderCommandHelp(cmd, width = null) {
let keysValues = [];
const keys = Setting.keys(true, 'cli');
for (let i = 0; i < keys.length; i++) {
if (keysValues.length) keysValues.push(['','']);
if (keysValues.length) keysValues.push(['', '']);
const md = Setting.settingMetadata(keys[i]);
if (!md.label) continue;
keysValues.push(renderMetadata(md));
@@ -91,8 +88,8 @@ function renderCommandHelp(cmd, width = null) {
output.push(renderTwoColumnData(keysValues, baseIndent, width));
}
return output.join("\n");
return output.join('\n');
}
function getOptionColWidth(options) {
@@ -104,4 +101,4 @@ function getOptionColWidth(options) {
return output;
}
module.exports = { renderCommandHelp };
module.exports = { renderCommandHelp };

View File

@@ -9,7 +9,7 @@ require('app-module-path').addPath(__dirname);
const compareVersion = require('compare-version');
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
if (compareVersion(nodeVersion, '8.0.0') < 0) {
console.error('Joplin requires Node 8+. Detected version ' + nodeVersion);
console.error(`Joplin requires Node 8+. Detected version ${nodeVersion}`);
process.exit(1);
}
@@ -53,25 +53,25 @@ shimInit();
const application = app();
if (process.platform === "win32") {
var rl = require("readline").createInterface({
if (process.platform === 'win32') {
var rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
rl.on("SIGINT", function () {
process.emit("SIGINT");
rl.on('SIGINT', function() {
process.emit('SIGINT');
});
}
process.stdout.on('error', function( err ) {
process.stdout.on('error', function(err) {
// https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508
if (err.code == "EPIPE") {
if (err.code == 'EPIPE') {
process.exit(0);
}
});
application.start(process.argv).catch((error) => {
application.start(process.argv).catch(error => {
if (error.code == 'flagError') {
console.error(error.message);
console.error(_('Type `joplin help` for usage information.'));
@@ -81,4 +81,4 @@ application.start(process.argv).catch((error) => {
}
process.exit(1);
});
});

View File

@@ -1,13 +1,11 @@
const { _ } = require('lib/locale.js');
const { netUtils } = require('lib/net-utils.js');
const http = require("http");
const urlParser = require("url");
const FormData = require('form-data');
const http = require('http');
const urlParser = require('url');
const enableServerDestroy = require('server-destroy');
class OneDriveApiNodeUtils {
constructor(api) {
this.api_ = api;
this.oauthServer_ = null;
@@ -46,9 +44,9 @@ class OneDriveApiNodeUtils {
const port = await netUtils.findAvailablePort(this.possibleOAuthDancePorts(), 0);
if (!port) throw new Error(_('All potential ports are in use - please report the issue at %s', 'https://github.com/laurent22/joplin'));
let authCodeUrl = this.api().authCodeUrl('http://localhost:' + port);
let authCodeUrl = this.api().authCodeUrl(`http://localhost:${port}`);
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
this.oauthServer_ = http.createServer();
let errorMessage = null;
@@ -56,7 +54,7 @@ class OneDriveApiNodeUtils {
const url = urlParser.parse(request.url, true);
if (url.pathname === '/auth') {
response.writeHead(302, { 'Location': authCodeUrl });
response.writeHead(302, { Location: authCodeUrl });
response.end();
return;
}
@@ -64,10 +62,10 @@ class OneDriveApiNodeUtils {
const query = url.query;
const writeResponse = (code, message) => {
response.writeHead(code, {"Content-Type": "text/html"});
response.writeHead(code, { 'Content-Type': 'text/html' });
response.write(this.makePage(message));
response.end();
}
};
// After the response has been received, don't destroy the server right
// away or the browser might display a connection reset error (even
@@ -77,21 +75,24 @@ class OneDriveApiNodeUtils {
this.oauthServer_.destroy();
this.oauthServer_ = null;
}, 1000);
}
};
if (!query.code) return writeResponse(400, '"code" query parameter is missing');
this.api().execTokenRequest(query.code, 'http://localhost:' + port.toString()).then(() => {
writeResponse(200, _('The application has been authorised - you may now close this browser tab.'));
targetConsole.log('');
targetConsole.log(_('The application has been successfully authorised.'));
waitAndDestroy();
}).catch((error) => {
writeResponse(400, error.message);
targetConsole.log('');
targetConsole.log(error.message);
waitAndDestroy();
});
this.api()
.execTokenRequest(query.code, `http://localhost:${port.toString()}`)
.then(() => {
writeResponse(200, _('The application has been authorised - you may now close this browser tab.'));
targetConsole.log('');
targetConsole.log(_('The application has been successfully authorised.'));
waitAndDestroy();
})
.catch(error => {
writeResponse(400, error.message);
targetConsole.log('');
targetConsole.log(error.message);
waitAndDestroy();
});
});
this.oauthServer_.on('close', () => {
@@ -113,10 +114,9 @@ class OneDriveApiNodeUtils {
targetConsole.log(_('Please open the following URL in your browser to authenticate the application. The application will create a directory in "Apps/Joplin" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.'));
targetConsole.log('');
targetConsole.log('http://127.0.0.1:' + port + '/auth');
targetConsole.log(`http://127.0.0.1:${port}/auth`);
});
}
}
module.exports = { OneDriveApiNodeUtils };
module.exports = { OneDriveApiNodeUtils };

View File

@@ -1,405 +0,0 @@
# Joplin
[![Donate](https://joplinapp.org/images/badges/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) [![Become a patron](https://joplinapp.org/images/badges/Patreon-Badge.svg)](https://www.patreon.com/joplin) [![Travis Build Status](https://travis-ci.org/laurent22/joplin.svg?branch=master)](https://travis-ci.org/laurent22/joplin) [![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/github/laurent22/joplin?branch=master&passingText=master%20-%20OK&svg=true)](https://ci.appveyor.com/project/laurent22/joplin)
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
Notes exported from Evernote via .enex files [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be [synchronised](#synchronisation) with various cloud services including [Nextcloud](https://nextcloud.com/), Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). 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 application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
<div class="top-screenshot"><img src="https://joplinapp.org/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
# Installation
Three types of applications are available: for the **desktop** (Windows, macOS and Linux), for **mobile** (Android and iOS) and for **terminal** (Windows, macOS and Linux). All applications have similar user interfaces and can synchronise with each other.
## Desktop applications
Operating System | Download | Alternative
-----------------|--------|-------------------
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-Setup-1.0.143.exe'><img alt='Get it on Windows' height="40px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-1.0.143.dmg'><img alt='Get it on macOS' height="40px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> |
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-1.0.143-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package<br>[is also available](#terminal-application).
The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
On Linux, if it works with your distribution (it has been tested on Ubuntu, Fedora, Gnome and Mint), the recommended way is to use this script as it will handle the desktop icon too:
``` sh
wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh | bash
```
## 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://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.243/joplin-v1.0.243.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
## Terminal application
Operating system | Method
-----------------|----------------
macOS | `brew install joplin`
Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
Arch Linux | An Arch Linux package is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild).
To start it, type `joplin`.
For usage information, please refer to the full [Joplin Terminal Application Documentation](https://joplinapp.org/terminal/).
## Web Clipper
The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. For more information on how to install and use it, see the [Web Clipper Help Page](https://github.com/laurent22/joplin/blob/master/readme/clipper.md).
<!-- TOC -->
# Table of contents
- Applications
- [Desktop application](https://github.com/laurent22/joplin/blob/master/readme/desktop.md)
- [Mobile applications](https://github.com/laurent22/joplin/blob/master/readme/mobile.md)
- [Terminal application](https://github.com/laurent22/joplin/blob/master/readme/terminal.md)
- [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md)
- Support
- [Joplin Forum](https://discourse.joplin.cozic.net)
- [How to enable end-to-end encryption](https://github.com/laurent22/joplin/blob/master/readme/e2ee.md)
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md)
- [How to enable debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md)
- [API documentation](https://github.com/laurent22/joplin/blob/master/readme/api.md)
- [FAQ](https://github.com/laurent22/joplin/blob/master/readme/faq.md)
- About
- [Changelog](https://github.com/laurent22/joplin/blob/master/readme/changelog.md)
- [Stats](https://github.com/laurent22/joplin/blob/master/readme/stats.md)
- [Donate](https://github.com/laurent22/joplin/blob/master/readme/donate.md)
<!-- TOC -->
# Features
- Desktop, mobile and terminal applications.
- [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md) for Firefox and Chrome.
- End To End Encryption (E2EE)
- Synchronisation with various services, including NextCloud, Dropbox, WebDAV and OneDrive.
- Import Enex files (Evernote export format) and Markdown files.
- Export JEX files (Joplin Export format) and raw files.
- Support notes, to-dos, tags and notebooks.
- Sort notes by multiple criteria - title, updated time, etc.
- 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.
- 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
- External editor support - open notes in your favorite external editor with one click in Joplin.
# Importing
## Importing from Evernote
Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, resources (attached files) and note metadata (such as author, geo-location, etc.) via ENEX files. In terms of data, the only two things that might slightly differ are:
- Recognition data - Evernote images, in particular scanned (or photographed) documents have [recognition data](https://en.wikipedia.org/wiki/Optical_character_recognition) associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, should it become supported in the search tool or other parts of Joplin, it should be possible to regenerate this recognition data since the actual image would still be available.
- Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be lost - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting.
To import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then follow these steps:
On the **desktop application**, open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc.
On the **terminal application**, in [command-line mode](https://joplinapp.org/terminal/#command-line-mode), type `import /path/to/file.enex`. This will import the notes into a new notebook named after the filename.
## Importing from Markdown files
Joplin can import notes from plain Markdown file. You can either import a complete directory of Markdown files or individual files.
On the **desktop application**, open File > Import > MD and select your Markdown file or directory.
On the **terminal application**, in [command-line mode](https://joplinapp.org/terminal/#command-line-mode), type `import --format md /path/to/file.md` or `import --format md /path/to/directory/`.
## Importing from other applications
In general the way to import notes from any application into Joplin is to convert the notes to ENEX files (Evernote format) and to import these ENEX files into Joplin using the method above. Most note-taking applications support ENEX files so it should be relatively straightforward. For help about specific applications, see below:
* Standard Notes: Please see [this tutorial](https://programadorwebvalencia.com/migrate-notes-from-standard-notes-to-joplin/)
* Tomboy Notes: Export the notes to ENEX files [as described here](https://askubuntu.com/questions/243691/how-can-i-export-my-tomboy-notes-into-evernote/608551) for example, and import these ENEX files into Joplin.
* OneNote: First [import the notes from OneNote into Evernote](https://discussion.evernote.com/topic/107736-is-there-a-way-to-import-from-onenote-into-evernote-on-the-mac/). Then export the ENEX file from Evernote and import it into Joplin.
* NixNote: Synchronise with Evernote, then export the ENEX files and import them into Joplin. More info [in this thread](https://discourse.joplin.cozic.net/t/import-from-nixnote/183/3).
# Exporting
Joplin can export to the JEX format (Joplin Export file), which is a tar file that can contain multiple notes, notebooks, etc. This is a lossless format in that all the notes, but also metadata such as geo-location, updated time, tags, etc. are preserved. This format is convenient for backup purposes and can be re-imported into Joplin. A "raw" format is also available. This is the same as the JEX format except that the data is saved to a directory and each item represented by a single file.
# 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 Nextcloud 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.
Currently, synchronisation is possible with Nextcloud, Dropbox (by default), OneDrive or the local filesystem. 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.
## Nextcloud synchronisation
<img src="https://joplinapp.org/images/nextcloud-logo-background.png" width="100" align="left"> <a href="https://nextcloud.com/">Nextcloud</a> is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin. The platform is also well supported and with a strong community, so it is likely to be around for a while - since it's open source anyway, it is not a service that can be closed, it can exist on a server for as long as one chooses.
On the **desktop application** or **mobile application**, go to the config screen and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, click on Settings in the bottom left corner of the page, in Nextcloud), this is normally `https://example.com/nextcloud/remote.php/webdav/Joplin` (**make sure to create the "Joplin" directory in Nextcloud**), and set the username and password. If it does not work, please [see this explanation](https://github.com/laurent22/joplin/issues/61#issuecomment-373282608) for more details.
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/webdav/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.
## Dropbox synchronisation
When syncing with Dropbox, Joplin creates a sub-directory in Dropbox, 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 "Dropbox" 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 and follow the instructions.
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application. 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
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
WebDAV-compatible services that are known to work with Joplin:
- [Apache WebDAV Module](https://httpd.apache.org/docs/current/mod/mod_dav.html)
- [Box.com](https://www.box.com/)
- [DriveHQ](https://www.drivehq.com)
- [Fastmail](https://www.fastmail.com/)
- [HiDrive](https://www.strato.fr/stockage-en-ligne/) from Strato. [Setup help](https://github.com/laurent22/joplin/issues/309)
- [Nginx WebDAV Module](https://nginx.org/en/docs/http/ngx_http_dav_module.html)
- [NextCloud](https://nextcloud.com/)
- [OwnCloud](https://owncloud.org/)
- [Seafile](https://www.seafile.com/)
- [Stack](https://www.transip.nl/stack/)
- [WebDAV Nav](https://www.schimera.com/products/webdav-nav-server/), a macOS server.
- [Zimbra](https://www.zimbra.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. Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar and follow the instructions.
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).
# Encryption
Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](https://joplinapp.org/e2ee/) for more information about this feature and how to enable it.
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](https://joplinapp.org/spec/).
# External text editor
Joplin notes can be opened and edited using an external editor of your choice. It can be a simple text editor like Notepad++ or Sublime Text or an actual Markdown editor like Typora. In that case, images will also be displayed within the editor. To open the note in an external editor, click on the icon in the toolbar or press Ctrl+E (or Cmd+E). Your default text editor will be used to open the note. If needed, you can also specify the editor directly in the General Options, under "Text editor command".
# Attachments / Resources
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
On the **desktop application**, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.
Resources that are not attached to any note will be automatically deleted after 10 days (see [rationale](https://github.com/laurent22/joplin/issues/154#issuecomment-356582366)).
**Important:** Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.
# Notifications
On the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:
- **Windows**: >= 8. Make sure the Action Center is enabled on Windows. Task bar balloon for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.
- **macOS**: >= 10.8 or Growl if earlier.
- **Linux**: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default). Growl otherwise
See [documentation and flow chart for reporter choice](https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md)
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).
# Sub-notebooks
Sub-notebooks allow organising multiple notebooks into a tree of notebooks. For example it can be used to regroup all the notebooks related to work, to family or to a particular project under a parent notebook.
![](https://joplinapp.org/images/SubNotebooks.png)
- On the **desktop application**, to create a subnotebook, drag and drop it onto another notebook. To move it back to the root, drag and drop it on the "Notebooks" header. Currently only the desktop app can be used to organise the notebooks.
- The **mobile application** supports displaying and collapsing/expanding the tree of notebooks, however it does not currently support moving the subnotebooks to different notebooks.
- The **terminal app** supports displaying the tree of subnotebooks but it does not support collapsing/expanding them or moving the subnotebooks around.
# 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:
## Links to other notes
You can create a link to a note by specifying its ID in the URL. For example:
[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the **desktop app**, right click on a note an select "Copy Markdown link". In the **mobile app**, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.
## Plugins
Joplin supports a number of plugins that can be toggled on top the standard markdown features you would expect. These toggle-able plugins are listed below. Note: not all of the plugins are enabled by default, if the enable field is 'no' below, then enter Tools->General Options to enable the plugin. Plugins can be disabled in the same manner.
| Plugin | Syntax | Description | Enabled |
|--------|--------|-------------|---------|
| [Katex](https://katex.org) | `$$math expr$$` or `$math$` | [See Below](https://github.com/laurent22/joplin#math-notation) | yes |
| [Mark](https://github.com/markdown-it/markdown-it-mark) | `==marked==` | Transforms into `<mark>marked</mark>` (highlighted) | yes |
| [Footnote](https://github.com/markdown-it/markdown-it-footnote) | `Simples inline footnote ^[I'm inline!]` | See [plugin page](https://github.com/markdown-it/markdown-it-footnote) for full description | yes |
| [TOC](https://github.com/nagaozen/markdown-it-toc-done-right) | Any of `${toc}, [[toc]], [toc], [[_toc_]]` | Adds a table of contents to the location of the toc page. Based on headings and sub-headings | no |
| [Sub](https://github.com/markdown-it/markdown-it-sub) | `X~1~` | Transforms into X<sub>1</sub> | no |
| [Sup](https://github.com/markdown-it/markdown-it-sup) | `X^2^` | Transforms into X<sup>2</sup> | no |
| [Deflist](https://github.com/markdown-it/markdown-it-deflist) | See [pandoc](http://johnmacfarlane.net/pandoc/README.html#definition-lists) page for syntax | Adds the html `<dl>` tag accessible through markdown | no |
| [Abbr](https://github.com/markdown-it/markdown-it-abbr) | *[HTML]: Hyper Text Markup Language | Allows definition of abbreviations that can be hovered over later for a full expansion | no |
| [Emoji](https://github.com/markdown-it/markdown-it-emoji) | `:smile:` :smile: | See [this list](https://gist.github.com/rxaviers/7360908) for more emoji | no |
| [Insert](https://github.com/markdown-it/markdown-it-ins) | `++inserted++` | Transforms into `<ins>inserted</ins>` (<ins>inserted</ins>) | no |
| [Multitable](https://github.com/RedBug312/markdown-it-multimd-table) | See [MultiMarkdown](https://fletcher.github.io/MultiMarkdown-6/syntax/tables.html) page | Adds more power and customization to markdown tables | no |
## 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://joplinapp.org/images/Katex.png" height="400px">
## Checkboxes
Checkboxes can be added like so:
- [ ] Milk
- [ ] Rice
- [ ] Eggs
The checkboxes can then be ticked in the mobile and desktop applications.
## HTML support
It is generally recommended to enter the notes as Markdown as it makes the notes easier to edit. However for cases where certain features aren't supported (such as strikethrough or to highlight text), you can also use HTML code directly. For example this would be a valid note:
This is <s>strikethrough text</s> mixed with regular **Markdown**.
## Custom CSS
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the Config screen for the exact path). This file supports standard CSS syntax. Note that this file is used only when display the notes, **not when printing or exporting to PDF**. This is because printing has a lot more restrictions (for example, printing white text over a black background is usually not wanted), so special rules are applied to make it look good when printing, and a userstyle.css would interfer with that.
# Searching
Joplin implements the SQLite Full Text Search (FTS4) extension. It means the content of all the notes is indexed in real time and search queries return results very fast. Both [Simple FTS Queries](https://www.sqlite.org/fts3.html#simple_fts_queries) and [Full-Text Index Queries](https://www.sqlite.org/fts3.html#full_text_index_queries) are supported. See below for the list of supported queries:
Search type | Description | Example
------------|-------------|---------
Single word | Returns all the notes that contain this term. | `dog`, `cat`
Multiples words | Returns all the notes that contain **all** these words, but not necessarily next to each other. | `dog cat` - will return any notes that contain the words "dog" and "cat" anywhere in the note, no necessarily in that order nor next to each others. It will **not** return results that contain "dog" or "cat" only.
Phrase query | Add double quotes to return the notes that contain exactly this phrase. | `"shopping list"` - will return the notes that contain these **exact terms** next to each others and in this order. It will **not** return for example a note that contain "going shopping with my list".
Prefix | Add a wildmark to return all the notes that contain a term with a specified prefix. | `swim*` - will return all the notes that contain eg. "swim", but also "swimming", "swimsuit", etc. IMPORTANT: The wildcard **can only be at the end** - it will be ignored at the beginning of a word (eg. `*swim`) and will be treated as a literal asterisk in the middle of a word (eg. `ast*rix`)
Field restricted | Add either `title:` or `body:` before a note to restrict your search to just the title, or just the body. | `title:shopping`, `body:egg`
Notes are sorted by "relevance". Currently it means the notes that contain the requested terms the most times are on top. For queries with multiple terms, it also matter how close to each others are the terms. This is a bit experimental so if you notice a search query that returns unexpected results, please report it in the forum, providing as much details as possible to replicate the issue.
# Donations
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
Please see the [donation page](https://joplinapp.org/donate/) for information on how to support the development of Joplin.
# Community
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
- Also see here for information about [the latest releases and general news](https://discourse.joplin.cozic.net/c/news).
- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).
- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).
# 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 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).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
This translation will apply to the three applications - desktop, mobile and terminal.
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.
Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ar.po) | عبد الناصر سعيد (as@althobaity.com) | 95%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 54%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po) | jmontane, 2018 | 78%
![](https://joplinapp.org/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 44%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 69%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 70%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Michael Sonntag (ms@editorei.de) | 100%
![](https://joplinapp.org/images/flags/country-4x3/gb.png) | English (UK) | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/us.png) | English (US) | [en_US](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_US.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Andros Fenollosa (andros@fenollosa.email) | 96%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
![](https://joplinapp.org/images/flags/es/galicia.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 69%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 86%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 55%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 83%
![](https://joplinapp.org/images/flags/country-4x3/no.png) | Norwegian | [nb_NO](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nb_NO.po) | Mats Estensen (code@mxe.no) | 96%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 90%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 54%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 68%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po) | Jonatan Nyberg (jonatan@autistici.org) | 93%
![](https://joplinapp.org/images/flags/country-4x3/tr.png) | Türkçe | [tr_TR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/tr_TR.po) | Zorbey Doğangüneş (zorbeyd@gmail.com) | 90%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 96%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 96%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 83%
![](https://joplinapp.org/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | AWASHIRO Ikuya (ikunya@gmail.com) | 90%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 92%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs
- Resources larger than 10 MB are not currently supported on mobile. They will crash the application so it is recommended not to attach such resources at the moment. The issue is being looked at.
- 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.
- It is only possible to upload files of up to 4MB to OneDrive due to a limitation of [the API](https://docs.microsoft.com/en-gb/onedrive/developer/rest-api/api/driveitem_put_content) being currently used. There is currently no plan to support OneDrive "large file" API.
# License
MIT License
Copyright (c) 2016-2019 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:
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.

View File

@@ -1,97 +0,0 @@
const { _ } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const Resource = require('lib/models/Resource.js');
const { netUtils } = require('lib/net-utils.js');
const http = require("http");
const urlParser = require("url");
const enableServerDestroy = require('server-destroy');
class ResourceServer {
constructor() {
this.server_ = null;
this.logger_ = new Logger();
this.port_ = null;
this.linkHandler_ = null;
this.started_ = false;
}
setLogger(logger) {
this.logger_ = logger;
}
logger() {
return this.logger_;
}
started() {
return this.started_;
}
baseUrl() {
if (!this.port_) return '';
return 'http://127.0.0.1:' + this.port_;
}
setLinkHandler(handler) {
this.linkHandler_ = handler;
}
async start() {
this.port_ = await netUtils.findAvailablePort([9167, 9267, 8167, 8267]);
if (!this.port_) {
this.logger().error('Could not find available port to start resource server. Please report the error at https://github.com/laurent22/joplin');
return;
}
this.server_ = http.createServer();
this.server_.on('request', async (request, response) => {
const writeResponse = (message) => {
response.write(message);
response.end();
}
const url = urlParser.parse(request.url, true);
let resourceId = url.pathname.split('/');
if (resourceId.length < 2) {
writeResponse('Error: could not get resource ID from path name: ' + url.pathname);
return;
}
resourceId = resourceId[1];
if (!this.linkHandler_) throw new Error('No link handler is defined');
try {
const done = await this.linkHandler_(resourceId, response);
if (!done) throw new Error('Unhandled resource: ' + resourceId);
} catch (error) {
response.setHeader('Content-Type', 'text/plain');
response.statusCode = 400;
response.write(error.message);
}
response.end();
});
this.server_.on('error', (error) => {
this.logger().error('Resource server:', error);
});
this.server_.listen(this.port_);
enableServerDestroy(this.server_);
this.started_ = true;
}
stop() {
if (this.server_) this.server_.destroy();
this.server_ = null;
}
}
module.exports = ResourceServer;

View File

@@ -1,822 +0,0 @@
const { Logger } = require('lib/logger.js');
const Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const { cliUtils } = require('./cli-utils.js');
const { reducer, defaultState } = require('lib/reducer.js');
const { splitCommandString } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const chalk = require('chalk');
const tk = require('terminal-kit');
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
const Renderer = require('tkwidgets/framework/Renderer.js');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseWidget = require('tkwidgets/BaseWidget.js');
const ListWidget = require('tkwidgets/ListWidget.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const HLayoutWidget = require('tkwidgets/HLayoutWidget.js');
const VLayoutWidget = require('tkwidgets/VLayoutWidget.js');
const ReduxRootWidget = require('tkwidgets/ReduxRootWidget.js');
const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js');
const NoteWidget = require('./gui/NoteWidget.js');
const ResourceServer = require('./ResourceServer.js');
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js');
const StatusBarWidget = require('./gui/StatusBarWidget.js');
const ConsoleWidget = require('./gui/ConsoleWidget.js');
class AppGui {
constructor(app, store, keymap) {
try {
this.app_ = app;
this.store_ = store;
BaseWidget.setLogger(app.logger());
this.term_ = new TermWrapper(tk.terminal);
// 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_ = null;
this.logger_ = new Logger();
this.buildUi();
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
this.app_.on('modelAction', async (event) => {
await this.handleModelAction(event.action);
});
this.keymap_ = this.setupKeymap(keymap);
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
this.commandCancelCalled_ = false;
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() {
return this.store_;
}
renderer() {
return this.renderer_;
}
async forceRender() {
this.widget('root').invalidate();
await this.renderer_.renderRoot();
}
termSaveState() {
return this.term().saveState();
}
termRestoreState(state) {
return this.term().restoreState(state);
}
prompt(initialText = '', promptString = ':', options = null) {
return this.widget('statusBar').prompt(initialText, promptString, options);
}
stdoutMaxWidth() {
return this.widget('console').innerWidth - 1;
}
isDummy() {
return false;
}
buildUi() {
this.rootWidget_ = new ReduxRootWidget(this.store_);
this.rootWidget_.name = 'root';
this.rootWidget_.autoShortcutsEnabled = false;
const folderList = new FolderListWidget();
folderList.style = {
borderBottomWidth: 1,
borderRightWidth: 1,
};
folderList.name = 'folderList';
folderList.vStretch = true;
folderList.on('currentItemChange', async (event) => {
const item = folderList.currentItem;
if (item === '-') {
let newIndex = event.currentIndex + (event.previousIndex < event.currentIndex ? +1 : -1);
let nextItem = folderList.itemAt(newIndex);
if (!nextItem) nextItem = folderList.itemAt(event.previousIndex);
if (!nextItem) return; // Normally not possible
let actionType = 'FOLDER_SELECT';
if (nextItem.type_ === BaseModel.TYPE_TAG) actionType = 'TAG_SELECT';
if (nextItem.type_ === BaseModel.TYPE_SEARCH) actionType = 'SEARCH_SELECT';
this.store_.dispatch({
type: actionType,
id: nextItem.id,
});
} else if (item.type_ === Folder.modelType()) {
this.store_.dispatch({
type: 'FOLDER_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === Tag.modelType()) {
this.store_.dispatch({
type: 'TAG_SELECT',
id: item ? item.id : null,
});
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
this.store_.dispatch({
type: 'SEARCH_SELECT',
id: item ? item.id : null,
});
}
});
this.rootWidget_.connect(folderList, (state) => {
return {
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
notesParentType: state.notesParentType,
folders: state.folders,
tags: state.tags,
searches: state.searches,
};
});
const noteList = new NoteListWidget();
noteList.name = 'noteList';
noteList.vStretch = true;
noteList.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
noteList.on('currentItemChange', async () => {
let note = noteList.currentItem;
this.store_.dispatch({
type: 'NOTE_SELECT',
id: note ? note.id : null,
});
});
this.rootWidget_.connect(noteList, (state) => {
return {
selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
items: state.notes,
};
});
const noteText = new NoteWidget();
noteText.hStretch = true;
noteText.name = 'noteText';
noteText.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
};
this.rootWidget_.connect(noteText, (state) => {
return {
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
notes: state.notes,
};
});
const noteMetadata = new NoteMetadataWidget();
noteMetadata.hStretch = true;
noteMetadata.name = 'noteMetadata';
noteMetadata.style = {
borderBottomWidth: 1,
borderLeftWidth: 1,
borderRightWidth: 1,
};
this.rootWidget_.connect(noteMetadata, (state) => {
return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null };
});
noteMetadata.hide();
const consoleWidget = new ConsoleWidget();
consoleWidget.hStretch = true;
consoleWidget.style = {
borderBottomWidth: 1,
};
consoleWidget.hide();
const statusBar = new StatusBarWidget();
statusBar.hStretch = true;
const noteLayout = new VLayoutWidget();
noteLayout.name = 'noteLayout';
noteLayout.addChild(noteText, { type: 'stretch', factor: 1 });
noteLayout.addChild(noteMetadata, { type: 'stretch', factor: 1 });
const hLayout = new HLayoutWidget();
hLayout.name = 'hLayout';
hLayout.addChild(folderList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteList, { type: 'stretch', factor: 1 });
hLayout.addChild(noteLayout, { type: 'stretch', factor: 2 });
const vLayout = new VLayoutWidget();
vLayout.name = 'vLayout';
vLayout.addChild(hLayout, { type: 'stretch', factor: 2 });
vLayout.addChild(consoleWidget, { type: 'stretch', factor: 1 });
vLayout.addChild(statusBar, { type: 'fixed', factor: 1 });
const win1 = new WindowWidget();
win1.addChild(vLayout);
win1.name = 'mainWindow';
this.rootWidget_.addChild(win1);
}
showModalOverlay(text) {
if (!this.widget('overlayWindow')) {
const textWidget = new TextWidget();
textWidget.hStretch = true;
textWidget.vStretch = true;
textWidget.text = 'testing';
textWidget.name = 'overlayText';
const win = new WindowWidget();
win.name = 'overlayWindow';
win.addChild(textWidget);
this.rootWidget_.addChild(win);
}
this.widget('overlayWindow').activate();
this.widget('overlayText').text = text;
}
hideModalOverlay() {
if (this.widget('overlayWindow')) this.widget('overlayWindow').hide();
this.widget('mainWindow').activate();
}
addCommandToConsole(cmd) {
if (!cmd) return;
const isConfigPassword = cmd.indexOf('config ') >= 0 && cmd.indexOf('password') >= 0;
if (isConfigPassword) return;
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupKeymap(keymap) {
const output = [];
for (let i = 0; i < keymap.length; i++) {
const item = Object.assign({}, keymap[i]);
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
if (!('type' in item)) item.type = 'exec';
if (item.command in this.tkWidgetKeys_) {
item.type = 'tkwidgets';
}
item.canRunAlongOtherCommands = item.type === 'function' && ['toggle_metadata', 'toggle_console'].indexOf(item.command) >= 0;
output.push(item);
}
return output;
}
toggleConsole() {
this.showConsole(!this.consoleIsShown());
}
showConsole(doShow = true) {
this.widget('console').show(doShow);
}
hideConsole() {
this.showConsole(false);
}
consoleIsShown() {
return this.widget('console').shown;
}
maximizeConsole(doMaximize = true) {
const consoleWidget = this.widget('console');
if (consoleWidget.isMaximized__ === undefined) {
consoleWidget.isMaximized__ = false;
}
if (consoleWidget.isMaximized__ === doMaximize) return;
let constraints = {
type: 'stretch',
factor: !doMaximize ? 1 : 4,
};
consoleWidget.isMaximized__ = doMaximize;
this.widget('vLayout').setWidgetConstraints(consoleWidget, constraints);
}
minimizeConsole() {
this.maximizeConsole(false);
}
consoleIsMaximized() {
return this.widget('console').isMaximized__ === true;
}
showNoteMetadata(show = true) {
this.widget('noteMetadata').show(show);
}
hideNoteMetadata() {
this.showNoteMetadata(false);
}
toggleNoteMetadata() {
this.showNoteMetadata(!this.widget('noteMetadata').shown);
}
widget(name) {
if (name === 'root') return this.rootWidget_;
return this.rootWidget_.childByName(name);
}
app() {
return this.app_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
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() {
return this.term_;
}
activeListItem() {
const widget = this.widget('mainWindow').focusedWidget;
if (!widget) return null;
if (widget.name == 'noteList' || widget.name == 'folderList') {
return widget.currentItem;
}
return null;
}
async handleModelAction(action) {
this.logger().info('Action:', action);
let state = Object.assign({}, defaultState);
state.notes = this.widget('noteList').items;
let newState = reducer(state, action);
if (newState !== state) {
this.widget('noteList').items = newState.notes;
}
}
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().debug('Got command: ' + cmd);
try {
let note = this.widget('noteList').currentItem;
let folder = this.widget('folderList').currentItem;
let args = splitCommandString(cmd);
for (let i = 0; i < args.length; i++) {
if (args[i] == '$n') {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}
}
await this.app().execCommand(args);
} catch (error) {
this.stdout(error.message);
}
this.widget('console').scrollBottom();
// Invalidate so that the screen is redrawn in case inputting a command has moved
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
this.widget('root').invalidate();
}
async updateFolderList() {
const folders = await Folder.all();
this.widget('folderList').items = folders;
}
async updateNoteList(folderId) {
const fields = Note.previewFields();
fields.splice(fields.indexOf('body'), 1);
const notes = folderId ? await Note.previews(folderId, { fields: fields }) : [];
this.widget('noteList').items = notes;
}
async updateNoteText(note) {
const text = note ? note.body : '';
this.widget('noteText').text = text;
}
// Any key after which a shortcut is not possible.
isSpecialKey(name) {
return [':', 'ENTER', 'DOWN', 'UP', 'LEFT', 'RIGHT', 'DELETE', 'BACKSPACE', 'ESCAPE', 'TAB', 'SHIFT_TAB', 'PAGE_UP', 'PAGE_DOWN'].indexOf(name) >= 0;
}
fullScreen(enable = true) {
if (enable) {
this.term().fullscreen();
this.term().hideCursor();
this.widget('root').invalidate();
} else {
this.term().fullscreen(false);
this.term().showCursor();
}
}
stdout(text) {
if (text === null || text === undefined) return;
let lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const v = typeof lines[i] === 'object' ? JSON.stringify(lines[i]) : lines[i];
this.widget('console').addLine(v);
}
this.updateStatusBarMessage();
}
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
updateStatusBarMessage() {
const consoleWidget = this.widget('console');
let msg = '';
const text = consoleWidget.lastLine;
const cmd = this.app().currentCommand();
if (cmd) {
msg += cmd.name();
if (cmd.cancellable()) msg += ' [Press Ctrl+C to cancel]';
msg += ': ';
}
if (text && text.length) {
msg += text;
}
if (msg !== '') this.widget('statusBar').setItemAt(0, msg);
}
async setupResourceServer() {
const linkStyle = chalk.blue.underline;
const noteTextWidget = this.widget('noteText');
const resourceIdRegex = /^:\/[a-f0-9]+$/i
const noteLinks = {};
const hasProtocol = function(s, protocols) {
if (!s) return false;
s = s.trim().toLowerCase();
for (let i = 0; i < protocols.length; i++) {
if (s.indexOf(protocols[i] + '://') === 0) return true;
}
return false;
}
// By default, before the server is started, only the regular
// URLs appear in blue.
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (index, url) => {
if (!url) return url;
if (resourceIdRegex.test(url)) {
return url;
} else if (hasProtocol(url, ['http', 'https'])) {
return linkStyle(url);
} else {
return url;
}
},
};
this.resourceServer_ = new ResourceServer();
this.resourceServer_.setLogger(this.app().logger());
this.resourceServer_.setLinkHandler(async (path, response) => {
const link = noteLinks[path];
if (link.type === 'url') {
response.writeHead(302, { 'Location': link.url });
return true;
}
if (link.type === 'item') {
const itemId = link.id;
let item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error('No item with ID ' + itemId); // Should be nearly impossible
if (item.type_ === BaseModel.TYPE_RESOURCE) {
if (item.mime) response.setHeader('Content-Type', item.mime);
response.write(await Resource.content(item));
} else if (item.type_ === BaseModel.TYPE_NOTE) {
const html = [`
<!DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head><meta charset="UTF-8"/></head><body>
`];
html.push('<pre>' + htmlentities(item.title) + '\n\n' + htmlentities(item.body) + '</pre>');
html.push('</body></html>');
response.write(html.join(''));
} else {
throw new Error('Unsupported item type: ' + item.type_);
}
return true;
}
return false;
});
await this.resourceServer_.start();
if (!this.resourceServer_.started()) return;
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (index, url) => {
if (!url) return url;
if (resourceIdRegex.test(url)) {
noteLinks[index] = {
type: 'item',
id: url.substr(2),
};
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {
noteLinks[index] = {
type: 'url',
url: url,
};
} else if (url.indexOf('#') === 0) {
return ''; // Anchors aren't supported for now
} else {
return url;
}
return linkStyle(this.resourceServer_.baseUrl() + '/' + index);
},
};
}
async start() {
const term = this.term();
this.fullScreen();
try {
this.setupResourceServer();
this.renderer_.start();
const statusBar = this.widget('statusBar');
term.grabInput();
term.on('key', async (name, matches, data) => {
// -------------------------------------------------------------------------
// Handle special shortcuts
// -------------------------------------------------------------------------
if (name === 'CTRL_D') {
const cmd = this.app().currentCommand();
if (cmd && cmd.cancellable() && !this.commandCancelCalled_) {
this.commandCancelCalled_ = true;
await cmd.cancel();
this.commandCancelCalled_ = false;
}
await this.app().exit();
return;
}
if (name === 'CTRL_C' ) {
const cmd = this.app().currentCommand();
if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) {
this.stdout(_('Press Ctrl+D or type "exit" to exit the application'));
} else {
this.commandCancelCalled_ = true;
await cmd.cancel()
this.commandCancelCalled_ = false;
}
return;
}
// -------------------------------------------------------------------------
// Build up current shortcut
// -------------------------------------------------------------------------
const now = (new Date()).getTime();
if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) {
this.currentShortcutKeys_ = [name];
} else {
// If the previous key was a special key (eg. up, down arrow), this new key
// starts a new shortcut.
if (this.currentShortcutKeys_.length && this.isSpecialKey(this.currentShortcutKeys_[0])) {
this.currentShortcutKeys_ = [name];
} else {
this.currentShortcutKeys_.push(name);
}
}
this.lastShortcutKeyTime_ = now;
// -------------------------------------------------------------------------
// Process shortcut and execute associated command
// -------------------------------------------------------------------------
const shortcutKey = this.currentShortcutKeys_.join('');
let keymapItem = this.keymapItemByKey(shortcutKey);
// If this command is an alias to another command, resolve to the actual command
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
if (statusBar.promptActive) processShortcutKeys = false;
if (processShortcutKeys) {
this.logger().debug('Shortcut:', shortcutKey, keymapItem);
this.currentShortcutKeys_ = [];
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));
}
}
// Optimisation: Update the status bar only
// if the user is not already typing a command:
if (!statusBar.promptActive) this.updateStatusBarMessage();
});
} catch (error) {
this.fullScreen(false);
this.logger().error(error);
console.error(error);
}
process.on('unhandledRejection', (reason, p) => {
this.fullScreen(false);
console.error('Unhandled promise rejection', p, 'reason:', reason);
process.exit(1);
});
}
}
AppGui.INPUT_MODE_NORMAL = 1;
AppGui.INPUT_MODE_META = 2;
module.exports = AppGui;

View File

@@ -1,451 +0,0 @@
const { BaseApplication } = require('lib/BaseApplication');
const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState } = require('lib/reducer.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const { Database } = require('lib/database.js');
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const ResourceService = require('lib/services/ResourceService');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const Setting = require('lib/models/Setting.js');
const { Logger } = require('lib/logger.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const Cache = require('lib/Cache');
const WelcomeUtils = require('lib/WelcomeUtils');
const RevisionService = require('lib/services/RevisionService');
class Application extends BaseApplication {
constructor() {
super();
this.showPromptString_ = true;
this.commands_ = {};
this.commandMetadata_ = null;
this.activeCommand_ = null;
this.allCommandsLoaded_ = false;
this.showStackTraces_ = false;
this.gui_ = null;
this.cache_ = new Cache();
}
gui() {
return this.gui_;
}
commandStdoutMaxWidth() {
return this.gui().stdoutMaxWidth();
}
async guessTypeAndLoadItem(pattern, options = null) {
let type = BaseModel.TYPE_NOTE;
if (pattern.indexOf('/') === 0) {
type = BaseModel.TYPE_FOLDER;
pattern = pattern.substr(1);
}
return this.loadItem(type, pattern, options);
}
async loadItem(type, pattern, options = null) {
let output = await this.loadItems(type, pattern, options);
if (output.length > 1) {
// output.sort((a, b) => { return a.user_updated_time < b.user_updated_time ? +1 : -1; });
// let answers = { 0: _('[Cancel]') };
// for (let i = 0; i < output.length; i++) {
// answers[i + 1] = output[i].title;
// }
// Not really useful with new UI?
throw new Error(_('More than one item match "%s". Please narrow down your query.', pattern));
// let msg = _('More than one item match "%s". Please select one:', pattern);
// const response = await cliUtils.promptMcq(msg, answers);
// if (!response) return null;
return output[response - 1];
} else {
return output.length ? output[0] : null;
}
}
async loadItems(type, pattern, options = null) {
if (type === 'folderOrNote') {
const folders = await this.loadItems(BaseModel.TYPE_FOLDER, pattern, options);
if (folders.length) return folders;
return await this.loadItems(BaseModel.TYPE_NOTE, pattern, options);
}
pattern = pattern ? pattern.toString() : '';
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) { // Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else { // Single item
let item = null;
if (type == BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
} else {
item = await ItemClass.loadByTitle(pattern);
}
if (item) return [item];
item = await ItemClass.load(pattern); // Load by id
if (item) return [item];
if (pattern.length >= 2) {
return await ItemClass.loadByPartialId(pattern);
}
}
return [];
}
stdout(text) {
return this.gui().stdout(text);
}
setupCommand(cmd) {
cmd.setStdout((text) => {
return this.stdout(text);
});
cmd.setDispatcher((action) => {
if (this.store()) {
return this.store().dispatch(action);
} else {
return (action) => {};
}
});
cmd.setPrompt(async (message, options) => {
if (!options) options = {};
if (!options.type) options.type = 'boolean';
if (!options.booleanAnswerDefault) options.booleanAnswerDefault = 'y';
if (!options.answers) options.answers = options.booleanAnswerDefault === 'y' ? [_('Y'), _('n')] : [_('N'), _('y')];
if (options.type == 'boolean') {
message += ' (' + options.answers.join('/') + ')';
}
let answer = await this.gui().prompt('', message + ' ', options);
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
} else {
return answer;
}
});
return cmd;
}
async exit(code = 0) {
const doExit = async () => {
this.gui().exit();
await super.exit(code);
};
// Give it a few seconds to cancel otherwise exit anyway
setTimeout(async () => {
await doExit();
}, 5000);
if (await reg.syncTarget().syncStarted()) {
this.stdout(_('Cancelling background synchronisation... Please wait.'));
const sync = await reg.syncTarget().synchronizer();
await sync.cancel();
}
await doExit();
}
commands(uiType = null) {
if (!this.allCommandsLoaded_) {
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
cmd = this.setupCommand(cmd);
this.commands_[cmd.name()] = cmd;
});
this.allCommandsLoaded_ = true;
}
if (uiType !== null) {
let temp = [];
for (let n in this.commands_) {
if (!this.commands_.hasOwnProperty(n)) continue;
const c = this.commands_[n];
if (!c.supportsUi(uiType)) continue;
temp[n] = c;
}
return temp;
}
return this.commands_;
}
async commandNames() {
const metadata = await this.commandMetadata();
let output = [];
for (let n in metadata) {
if (!metadata.hasOwnProperty(n)) continue;
output.push(n);
}
return output;
}
async commandMetadata() {
if (this.commandMetadata_) return this.commandMetadata_;
let output = await this.cache_.getItem('metadata');
if (output) {
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
}
const commands = this.commands();
output = {};
for (let n in commands) {
if (!commands.hasOwnProperty(n)) continue;
const cmd = commands[n];
output[n] = cmd.metadata();
}
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
}
hasGui() {
return this.gui() && !this.gui().isDummy();
}
findCommandByName(name) {
if (this.commands_[name]) return this.commands_[name];
let CommandClass = null;
try {
CommandClass = require(__dirname + '/command-' + name + '.js');
} catch (error) {
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
let e = new Error(_('No such command: %s', name));
e.type = 'notFound';
throw e;
} else {
throw error;
}
}
let cmd = new CommandClass();
cmd = this.setupCommand(cmd);
this.commands_[name] = cmd;
return this.commands_[name];
}
dummyGui() {
return {
isDummy: () => { return true; },
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
showConsole: () => {},
maximizeConsole: () => {},
stdout: (text) => { console.info(text); },
fullScreen: (b=true) => {},
exit: () => {},
showModalOverlay: (text) => {},
hideModalOverlay: () => {},
stdoutMaxWidth: () => { return 100; },
forceRender: () => {},
termSaveState: () => {},
termRestoreState: (state) => {},
};
}
async execCommand(argv) {
if (!argv.length) return this.execCommand(['help']);
// reg.logger().debug('execCommand()', argv);
const commandName = argv[0];
this.activeCommand_ = this.findCommandByName(commandName);
let outException = null;
try {
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
await this.activeCommand_.action(cmdArgs);
} catch (error) {
outException = error;
}
this.activeCommand_ = null;
if (outException) throw outException;
}
currentCommand() {
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);
cliUtils.setStdout((object) => {
return this.stdout(object);
});
await WelcomeUtils.install(this.dispatch.bind(this));
// If we have some arguments left at this point, it's a command
// so execute it.
if (argv.length) {
this.gui_ = this.dummyGui();
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
try {
await this.execCommand(argv);
} catch (error) {
if (this.showStackTraces_) {
console.error(error);
} else {
console.info(error.message);
}
process.exit(1);
}
await Setting.saveAll();
// Need to call exit() explicitely, otherwise Node wait for any timeout to complete
// https://stackoverflow.com/questions/18050095
process.exit(0);
} 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(), keymap);
this.gui_.setLogger(this.logger_);
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
await FoldersScreenUtils.refreshFolders();
const tags = await Tag.allWithNotes();
ResourceService.runInBackground();
RevisionService.instance().runInBackground();
this.dispatch({
type: 'TAG_UPDATE_ALL',
items: tags,
});
this.store().dispatch({
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
}
}
}
let application_ = null;
function app() {
if (application_) return application_;
application_ = new Application();
return application_;
}
module.exports = { app };

View File

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

View File

@@ -1,99 +0,0 @@
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
class BaseCommand {
constructor() {
this.stdout_ = null;
this.prompt_ = null;
}
usage() {
throw new Error('Usage not defined');
}
encryptionCheck(item) {
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
}
description() {
throw new Error('Description not defined');
}
async action(args) {
throw new Error('Action not defined');
}
compatibleUis() {
return ['cli', 'gui'];
}
supportsUi(ui) {
return this.compatibleUis().indexOf(ui) >= 0;
}
options() {
return [];
}
hidden() {
return false;
}
enabled() {
return true;
}
cancellable() {
return false;
}
async cancel() {}
name() {
let r = this.usage().split(' ');
return r[0];
}
setDispatcher(fn) {
this.dispatcher_ = fn;
}
dispatch(action) {
if (!this.dispatcher_) throw new Error('Dispatcher not defined');
return this.dispatcher_(action);
}
setStdout(fn) {
this.stdout_ = fn;
}
stdout(text) {
if (this.stdout_) this.stdout_(text);
}
setPrompt(fn) {
this.prompt_ = fn;
}
async prompt(message, options = null) {
if (!this.prompt_) throw new Error('Prompt is undefined');
return await this.prompt_(message, options);
}
metadata() {
return {
name: this.name(),
usage: this.usage(),
options: this.options(),
hidden: this.hidden(),
};
}
logger() {
return reg.logger();
}
}
module.exports = { BaseCommand };

View File

@@ -1,139 +0,0 @@
const fs = require('fs-extra');
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
const wrap_ = require('word-wrap');
const { _, setLocale, languageCode } = require('lib/locale.js');
const rootDir = dirname(dirname(__dirname));
const MAX_WIDTH = 78;
const INDENT = ' ';
function wrap(text, indent) {
return wrap_(text, {
width: MAX_WIDTH - indent.length,
indent: indent,
});
}
function renderOptions(options) {
let output = [];
const optionColWidth = getOptionColWidth(options);
for (let i = 0; i < options.length; i++) {
let option = options[i];
const flag = option[0];
const indent = INDENT + INDENT + ' '.repeat(optionColWidth + 2);
let r = wrap(option[1], indent);
r = r.substr(flag.length + (INDENT + INDENT).length);
r = INDENT + INDENT + flag + r;
output.push(r);
}
return output.join("\n");
}
function renderCommand(cmd) {
let output = [];
output.push(INDENT + cmd.usage());
output.push('');
output.push(wrap(cmd.description(), INDENT + INDENT));
const optionString = renderOptions(cmd.options());
if (optionString) {
output.push('');
output.push(optionString);
}
return output.join("\n");
}
function getCommands() {
let output = [];
fs.readdirSync(__dirname).forEach((path) => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path)
if (ext != 'js') return;
let CommandClass = require('./' + path);
let cmd = new CommandClass();
if (!cmd.enabled()) return;
if (cmd.hidden()) return;
output.push(cmd);
});
return output;
}
function getOptionColWidth(options) {
let output = 0;
for (let j = 0; j < options.length; j++) {
const option = options[j];
if (option[0].length > output) output = option[0].length;
}
return output;
}
function getHeader() {
let output = [];
output.push('NAME');
output.push('');
output.push(wrap('joplin - a note taking and to-do app with synchronisation capabilities'), INDENT);
output.push('');
output.push('DESCRIPTION');
output.push('');
let description = [];
description.push('Joplin is a note taking and to-do application, which can handle a large number of notes organised into notebooks.');
description.push('The notes are searchable, can be copied, tagged and modified with your own text editor.');
description.push("\n\n");
description.push('The notes can be synchronised with various target including the file system (for example with a network directory) or with Microsoft OneDrive.');
description.push("\n\n");
description.push('Notes exported from Evenotes via .enex files can be imported into Joplin, including the formatted content, resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).');
output.push(wrap(description.join(''), INDENT));
return output.join("\n");
}
function getFooter() {
let output = [];
output.push('WEBSITE');
output.push('');
output.push(INDENT + 'https://joplinapp.org');
output.push('');
output.push('LICENSE');
output.push('');
let filePath = rootDir + '/LICENSE_' + languageCode();
if (!fs.existsSync(filePath)) filePath = rootDir + '/LICENSE';
const licenseText = fs.readFileSync(filePath, 'utf8');
output.push(wrap(licenseText, INDENT));
return output.join("\n");
}
async function main() {
// setLocale('fr_FR');
const commands = getCommands();
let commandBlocks = [];
for (let i = 0; i < commands.length; i++) {
let cmd = commands[i];
commandBlocks.push(renderCommand(cmd));
}
const headerText = getHeader();
const commandsText = commandBlocks.join("\n\n");
const footerText = getFooter();
console.info(headerText + "\n\n" + 'USAGE' + "\n\n" + commandsText + "\n\n" + footerText);
}
main().catch((error) => {
console.error(error);
});

View File

@@ -1,249 +0,0 @@
"use strict"
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
const { dirname } = require('lib/path-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { JoplinDatabase } = require('lib/joplin-database.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection', p, 'reason:', reason);
});
const baseDir = dirname(__dirname) + '/tests/cli-integration';
const joplinAppPath = __dirname + '/main.js';
const logger = new Logger();
logger.addTarget('console');
logger.setLevel(Logger.LEVEL_ERROR);
const dbLogger = new Logger();
dbLogger.addTarget('console');
dbLogger.setLevel(Logger.LEVEL_INFO);
const db = new JoplinDatabase(new DatabaseDriverNode());
db.setLogger(dbLogger);
function createClient(id) {
return {
'id': id,
'profileDir': baseDir + '/client' + id,
};
}
const client = createClient(1);
function execCommand(client, command, options = {}) {
let exePath = 'node ' + joplinAppPath;
let cmd = exePath + ' --update-geolocation-disabled --env dev --profile ' + client.profileDir + ' ' + command;
logger.info(client.id + ': ' + command);
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
if (error) {
logger.error(stderr);
reject(error);
} else {
resolve(stdout.trim());
}
});
});
}
function assertTrue(v) {
if (!v) throw new Error(sprintf('Expected "true", got "%s"."', v));
process.stdout.write('.');
}
function assertFalse(v) {
if (v) throw new Error(sprintf('Expected "false", got "%s"."', v));
process.stdout.write('.');
}
function assertEquals(expected, real) {
if (expected !== real) throw new Error(sprintf('Expecting "%s", got "%s"', expected, real));
process.stdout.write('.');
}
async function clearDatabase() {
await db.transactionExecBatch([
'DELETE FROM folders',
'DELETE FROM notes',
'DELETE FROM tags',
'DELETE FROM note_tags',
'DELETE FROM resources',
'DELETE FROM deleted_items',
]);
}
const testUnits = {};
testUnits.testFolders = async () => {
await execCommand(client, 'mkbook nb1');
let folders = await Folder.all();
assertEquals(1, folders.length);
assertEquals('nb1', folders[0].title);
await execCommand(client, 'mkbook nb1');
folders = await Folder.all();
assertEquals(1, folders.length);
assertEquals('nb1', folders[0].title);
await execCommand(client, 'rm -r -f nb1');
folders = await Folder.all();
assertEquals(0, folders.length);
}
testUnits.testNotes = async () => {
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mknote n1');
let notes = await Note.all();
assertEquals(1, notes.length);
assertEquals('n1', notes[0].title);
await execCommand(client, 'rm -f n1');
notes = await Note.all();
assertEquals(0, notes.length);
await execCommand(client, 'mknote n1');
await execCommand(client, 'mknote n2');
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, "rm -f 'blabla*'");
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, "rm -f 'n*'");
notes = await Note.all();
assertEquals(0, notes.length);
}
testUnits.testCat = async () => {
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mknote mynote');
let folder = await Folder.loadByTitle('nb1');
let note = await Note.loadFolderNoteByField(folder.id, 'title', 'mynote');
let r = await execCommand(client, 'cat mynote');
assertTrue(r.indexOf('mynote') >= 0);
assertFalse(r.indexOf(note.id) >= 0);
r = await execCommand(client, 'cat -v mynote');
assertTrue(r.indexOf(note.id) >= 0);
}
testUnits.testConfig = async () => {
await execCommand(client, 'config editor vim');
await Setting.load();
assertEquals('vim', Setting.value('editor'));
await execCommand(client, 'config editor subl');
await Setting.load();
assertEquals('subl', Setting.value('editor'));
let r = await execCommand(client, 'config');
assertTrue(r.indexOf('editor') >= 0);
assertTrue(r.indexOf('subl') >= 0);
}
testUnits.testCp = async () => {
await execCommand(client, 'mkbook nb2');
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mknote n1');
await execCommand(client, 'cp n1');
let f1 = await Folder.loadByTitle('nb1');
let f2 = await Folder.loadByTitle('nb2');
let notes = await Note.previews(f1.id);
assertEquals(2, notes.length);
await execCommand(client, 'cp n1 nb2');
let notesF1 = await Note.previews(f1.id);
assertEquals(2, notesF1.length);
notes = await Note.previews(f2.id);
assertEquals(1, notes.length);
assertEquals(notesF1[0].title, notes[0].title);
}
testUnits.testLs = async () => {
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mknote note1');
await execCommand(client, 'mknote note2');
let r = await execCommand(client, 'ls');
assertTrue(r.indexOf('note1') >= 0);
assertTrue(r.indexOf('note2') >= 0);
}
testUnits.testMv = async () => {
await execCommand(client, 'mkbook nb2');
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mknote n1');
await execCommand(client, 'mv n1 nb2');
let f1 = await Folder.loadByTitle('nb1');
let f2 = await Folder.loadByTitle('nb2');
let notes1 = await Note.previews(f1.id);
let notes2 = await Note.previews(f2.id);
assertEquals(0, notes1.length);
assertEquals(1, notes2.length);
await execCommand(client, 'mknote note1');
await execCommand(client, 'mknote note2');
await execCommand(client, 'mknote note3');
await execCommand(client, 'mknote blabla');
await execCommand(client, "mv 'note*' nb2");
notes1 = await Note.previews(f1.id);
notes2 = await Note.previews(f2.id);
assertEquals(1, notes1.length);
assertEquals(4, notes2.length);
}
async function main(argv) {
await fs.remove(baseDir);
logger.info(await execCommand(client, 'version'));
await db.open({ name: client.profileDir + '/database.sqlite' });
BaseModel.db_ = db;
await Setting.load();
let onlyThisTest = 'testMv';
onlyThisTest = '';
for (let n in testUnits) {
if (!testUnits.hasOwnProperty(n)) continue;
if (onlyThisTest && n != onlyThisTest) continue;
await clearDatabase();
let testName = n.substr(4).toLowerCase();
process.stdout.write(testName + ': ');
await testUnits[n]();
console.info('');
}
}
main(process.argv).catch((error) => {
console.info('');
logger.error(error);
});

View File

@@ -1,250 +0,0 @@
const yargParser = require('yargs-parser');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const stringPadding = require('string-padding');
const cliUtils = {};
cliUtils.printArray = function(logFunction, rows, headers = null) {
if (!rows.length) return '';
const ALIGN_LEFT = 0;
const ALIGN_RIGHT = 1;
let colWidths = [];
let colAligns = [];
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
for (let j = 0; j < row.length; j++) {
let item = row[j];
let width = item ? item.toString().length : 0;
let align = typeof item == 'number' ? ALIGN_RIGHT : ALIGN_LEFT;
if (!colWidths[j] || colWidths[j] < width) colWidths[j] = width;
if (colAligns.length <= j) colAligns[j] = align;
}
}
let lines = [];
for (let row = 0; row < rows.length; row++) {
let line = [];
for (let col = 0; col < colWidths.length; col++) {
let item = rows[row][col];
let width = colWidths[col];
let dir = colAligns[col] == ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
line.push(stringPadding(item, width, ' ', dir));
}
logFunction(line.join(' '));
}
}
cliUtils.parseFlags = function(flags) {
let output = {};
flags = flags.split(',');
for (let i = 0; i < flags.length; i++) {
let f = flags[i].trim();
if (f.substr(0, 2) == '--') {
f = f.split(' ');
output.long = f[0].substr(2).trim();
if (f.length == 2) {
output.arg = cliUtils.parseCommandArg(f[1].trim());
}
} else if (f.substr(0, 1) == '-') {
output.short = f.substr(1);
}
}
return output;
}
cliUtils.parseCommandArg = function(arg) {
if (arg.length <= 2) throw new Error('Invalid command arg: ' + arg);
const c1 = arg[0];
const c2 = arg[arg.length - 1];
const name = arg.substr(1, arg.length - 2);
if (c1 == '<' && c2 == '>') {
return { required: true, name: name };
} else if (c1 == '[' && c2 == ']') {
return { required: false, name: name };
} else {
throw new Error('Invalid command arg: ' + arg);
}
}
cliUtils.makeCommandArgs = function(cmd, argv) {
let cmdUsage = cmd.usage();
cmdUsage = yargParser(cmdUsage);
let output = {};
let options = cmd.options();
let booleanFlags = [];
let aliases = {};
for (let i = 0; i < options.length; i++) {
if (options[i].length != 2) throw new Error('Invalid options: ' + options[i]);
let flags = options[i][0];
let text = options[i][1];
flags = cliUtils.parseFlags(flags);
if (!flags.arg) {
booleanFlags.push(flags.short);
if (flags.long) booleanFlags.push(flags.long);
}
if (flags.short && flags.long) {
aliases[flags.long] = [flags.short];
}
}
let args = yargParser(argv, {
boolean: booleanFlags,
alias: aliases,
string: ['_'],
});
for (let i = 1; i < cmdUsage['_'].length; i++) {
const a = cliUtils.parseCommandArg(cmdUsage['_'][i]);
if (a.required && !args['_'][i]) throw new Error(_('Missing required argument: %s', a.name));
if (i >= a.length) {
output[a.name] = null;
} else {
output[a.name] = args['_'][i];
}
}
let argOptions = {};
for (let key in args) {
if (!args.hasOwnProperty(key)) continue;
if (key == '_') continue;
argOptions[key] = args[key];
}
output.options = argOptions;
return output;
}
cliUtils.promptMcq = function(message, answers) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
message += "\n\n";
for (let n in answers) {
if (!answers.hasOwnProperty(n)) continue;
message += _('%s: %s', n, answers[n]) + "\n";
}
message += "\n";
message += _('Your choice: ');
return new Promise((resolve, reject) => {
rl.question(message, (answer) => {
rl.close();
if (!(answer in answers)) {
reject(new Error(_('Invalid answer: %s', answer)));
return;
}
resolve(answer);
});
});
}
cliUtils.promptConfirm = function(message, answers = null) {
if (!answers) answers = [_('Y'), _('n')];
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
message += ' (' + answers.join('/') + ')';
return new Promise((resolve, reject) => {
rl.question(message + ' ', (answer) => {
const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase();
rl.close();
resolve(ok);
});
});
}
// Note: initialText is there to have the same signature as statusBar.prompt() so that
// it can be a drop-in replacement, however initialText is not used (and cannot be
// with readline.question?).
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
if (!options) options = {};
const readline = require('readline');
const Writable = require('stream').Writable;
const mutableStdout = new Writable({
write: function(chunk, encoding, callback) {
if (!this.muted)
process.stdout.write(chunk, encoding);
callback();
}
});
const rl = readline.createInterface({
input: process.stdin,
output: mutableStdout,
terminal: true,
});
return new Promise((resolve, reject) => {
mutableStdout.muted = false;
rl.question(promptString, (answer) => {
rl.close();
if (!!options.secure) this.stdout_('');
resolve(answer);
});
mutableStdout.muted = !!options.secure;
});
}
let redrawStarted_ = false;
let redrawLastLog_ = null;
let redrawLastUpdateTime_ = 0;
cliUtils.setStdout = function(v) {
this.stdout_ = v;
}
cliUtils.redraw = function(s) {
const now = time.unixMs();
if (now - redrawLastUpdateTime_ > 4000) {
this.stdout_(s);
redrawLastUpdateTime_ = now;
redrawLastLog_ = null;
} else {
redrawLastLog_ = s;
}
redrawStarted_ = true;
}
cliUtils.redrawDone = function() {
if (!redrawStarted_) return;
if (redrawLastLog_) {
this.stdout_(redrawLastLog_);
}
redrawLastLog_ = null;
redrawStarted_ = false;
}
module.exports = { cliUtils };

View File

@@ -1,299 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem');
const BaseModel = require('lib/BaseModel');
const Setting = require('lib/models/Setting.js');
const { toTitleCase } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const markdownUtils = require('lib/markdownUtils');
const { Database } = require('lib/database.js');
class Command extends BaseCommand {
usage() {
return 'apidoc';
}
description() {
return 'Build the API doc';
}
createPropertiesTable(tableFields) {
const headers = [
{ name: 'name', label: 'Name' },
{ name: 'type', label: 'Type', filter: (value) => {
return Database.enumName('fieldType', value);
}},
{ name: 'description', label: 'Description' },
];
return markdownUtils.createMarkdownTable(headers, tableFields);
}
async action(args) {
const models = [
{
type: BaseModel.TYPE_NOTE,
},
{
type: BaseModel.TYPE_FOLDER,
},
{
type: BaseModel.TYPE_RESOURCE,
},
{
type: BaseModel.TYPE_TAG,
},
];
const lines = [];
lines.push('# Joplin API');
lines.push('');
lines.push('When the Web Clipper service is enabled, Joplin exposes a [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) which allows third-party applications to access Joplin\'s data and to create, modify or delete notes, notebooks, resources or tags.');
lines.push('');
lines.push('In order to use it, you\'ll first need to find on which port the service is running. To do so, open the Web Clipper Options in Joplin and if the service is running it should tell you on which port. Normally it runs on port **41184**. If you want to find it programmatically, you may follow this kind of algorithm:');
lines.push('');
lines.push('```javascript');
lines.push('let port = null;');
lines.push('for (let portToTest = 41184; portToTest <= 41194; portToTest++) {');
lines.push(' const result = pingPort(portToTest); // Call GET /ping');
lines.push(' if (result == \'JoplinClipperServer\') {');
lines.push(' port = portToTest; // Found the port');
lines.push(' break;');
lines.push(' }');
lines.push('}');
lines.push('```');
lines.push('');
lines.push('# Authorisation')
lines.push('');
lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.');
lines.push('');
lines.push('This would be an example of valid cURL call using a token:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123');
lines.push('');
lines.push('In the documentation below, the token will not be specified every time however you will need to include it.');
lines.push('');
lines.push('# Using the API');
lines.push('');
lines.push('All the calls, unless noted otherwise, receives and send **JSON data**. For example to create a new note:');
lines.push('');
lines.push('\tcurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://localhost:41184/notes');
lines.push('');
lines.push('In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.');
lines.push('');
lines.push('For example, for the endpoint `DELETE /tags/:id/notes/:note_id`, to remove the tag with ID "ABCD1234" from the note with ID "EFGH789", you would run for example:');
lines.push('');
lines.push('\tcurl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789');
lines.push('');
lines.push('The four verbs supported by the API are the following ones:');
lines.push('');
lines.push('* **GET**: To retrieve items (notes, notebooks, etc.).');
lines.push('* **POST**: To create new items. In general most item properties are optional. If you omit any, a default value will be used.');
lines.push('* **PUT**: To update an item. Note in a REST API, traditionally PUT is used to completely replace an item, however in this API it will only replace the properties that are provided. For example if you PUT {"title": "my new title"}, only the "title" property will be changed. The other properties will be left untouched (they won\'t be cleared nor changed).');
lines.push('* **DELETE**: To delete items.');
lines.push('');
lines.push('# Filtering data');
lines.push('');
lines.push('You can change the fields that will be returned by the API using the `fields=` query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude');
lines.push('');
lines.push('To get the IDs only of all the tags:');
lines.push('');
lines.push('\tcurl http://localhost:41184/tags?fields=id');
lines.push('');
lines.push('# Error handling');
lines.push('');
lines.push('In case of an error, an HTTP status code >= 400 will be returned along with a JSON object that provides more info about the error. The JSON object is in the format `{ "error": "description of error" }`.');
lines.push('');
lines.push('# About the property types');
lines.push('');
lines.push('* Text is UTF-8.');
lines.push('* All date/time are Unix timestamps in milliseconds.');
lines.push('* Booleans are integer values 0 or 1.');
lines.push('');
lines.push('# Testing if the service is available');
lines.push('');
lines.push('Call **GET /ping** to check if the service is available. It should return "JoplinClipperServer" if it works.');
lines.push('');
lines.push('# Searching');
lines.push('');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching');
lines.push('');
for (let i = 0; i < models.length; i++) {
const model = models[i];
const ModelClass = BaseItem.getClassByItemType(model.type);
const tableName = ModelClass.tableName();
let tableFields = reg.db().tableFields(tableName, { includeDescription: true });
const singular = tableName.substr(0, tableName.length - 1);
if (model.type === BaseModel.TYPE_NOTE) {
tableFields = tableFields.slice();
tableFields.push({
name: 'body_html',
type: Database.enumId('fieldType', 'text'),
description: 'Note body, in HTML format',
});
tableFields.push({
name: 'base_url',
type: Database.enumId('fieldType', 'text'),
description: 'If `body_html` is provided and contains relative URLs, provide the `base_url` parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the \'?\'). For example if the original page was `https://stackoverflow.com/search?q=%5Bjava%5D+test`, the base URL is `https://stackoverflow.com/search`.',
});
tableFields.push({
name: 'image_data_url',
type: Database.enumId('fieldType', 'text'),
description: 'An image to attach to the note, in [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.',
});
tableFields.push({
name: 'crop_rect',
type: Database.enumId('fieldType', 'text'),
description: 'If an image is provided, you can also specify an optional rectangle that will be used to crop the image. In format `{ x: x, y: y, width: width, height: height }`',
});
// tableFields.push({
// name: 'tags',
// type: Database.enumId('fieldType', 'text'),
// description: 'Comma-separated list of tags. eg. `tag1,tag2`.',
// });
}
lines.push('# ' + toTitleCase(tableName));
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('This is actually a notebook. Internally notebooks are called "folders".');
lines.push('');
}
lines.push('## Properties');
lines.push('');
lines.push(this.createPropertiesTable(tableFields));
lines.push('');
lines.push('## GET /' + tableName);
lines.push('');
lines.push('Gets all ' + tableName);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('The folders are returned as a tree. The sub-notebooks of a notebook, if any, are under the `children` key.');
lines.push('');
}
lines.push('## GET /' + tableName + '/:id');
lines.push('');
lines.push('Gets ' + singular + ' with ID :id');
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## GET /tags/:id/notes');
lines.push('');
lines.push('Gets all the notes with this tag.');
lines.push('');
}
if (model.type === BaseModel.TYPE_NOTE) {
lines.push('## GET /notes/:id/tags');
lines.push('');
lines.push('Gets all the tags attached to this note.');
lines.push('');
}
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('## GET /folders/:id/notes');
lines.push('');
lines.push('Gets all the notes inside this folder.');
lines.push('');
}
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('## GET /resources/:id/file');
lines.push('');
lines.push('Gets the actual file associated with this resource.');
lines.push('');
}
lines.push('## POST /' + tableName);
lines.push('');
lines.push('Creates a new ' + singular);
lines.push('');
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('Creating a new resource is special because you also need to upload the file. Unlike other API calls, this one must have the "multipart/form-data" Content-Type. The file data must be passed to the "data" form field, and the other properties to the "props" form field. An example of a valid call with cURL would be:');
lines.push('');
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
lines.push('');
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
lines.push('');
}
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## POST /tags/:id/notes');
lines.push('');
lines.push('Post a note to this endpoint to add the tag to the note. The note data must at least contain an ID property (all other properties will be ignored).');
lines.push('');
}
if (model.type === BaseModel.TYPE_NOTE) {
lines.push('You can either specify the note body as Markdown by setting the `body` parameter, or in HTML by setting the `body_html`.');
lines.push('');
lines.push('Examples:');
lines.push('');
lines.push('* Create a note from some Markdown text');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://127.0.0.1:41184/notes');
lines.push('');
lines.push('* Create a note from some HTML');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}\' http://127.0.0.1:41184/notes');
lines.push('');
lines.push('* Create a note and attach an image to it:');
lines.push('');
lines.push(' curl --data \'{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}\' http://127.0.0.1:41184/notes');
lines.push('');
lines.push('### Creating a note with a specific ID');
lines.push('');
lines.push('When a new note is created, it is automatically assigned a new unique ID so **normally you do not need to set the ID**. However, if for some reason you want to set it, you can supply it as the `id` property. It needs to be a 32 characters long hexadecimal string. **Make sure it is unique**, for example by generating it using whatever GUID function is available in your programming language.');
lines.push('');
lines.push(' curl --data \'{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}\' http://127.0.0.1:41184/notes');
lines.push('');
}
lines.push('## PUT /' + tableName + '/:id');
lines.push('');
lines.push('Sets the properties of the ' + singular + ' with ID :id');
lines.push('');
lines.push('## DELETE /' + tableName + '/:id');
lines.push('');
lines.push('Deletes the ' + singular + ' with ID :id');
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## DELETE /tags/:id/notes/:note_id');
lines.push('');
lines.push('Remove the tag from the note.');
lines.push('');
}
}
this.stdout(lines.join('\n'));
}
}
module.exports = Command;

View File

@@ -1,32 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'attach <note> <file>';
}
description() {
return _('Attaches the given file to the note.');
}
async action(args) {
let title = args['note'];
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
this.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', title));
const localFilePath = args['file'];
await shim.attachFileToNote(note, localFilePath);
}
}
module.exports = Command;

View File

@@ -1,39 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'cat <note>';
}
description() {
return _('Displays the given note.');
}
options() {
return [
['-v, --verbose', _('Displays the complete information about note.')],
];
}
async action(args) {
let title = args['note'];
let item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
this.stdout(content);
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@@ -1,70 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { _, setLocale } = require('lib/locale.js');
const { app } = require('./app.js');
const Setting = require('lib/models/Setting.js');
class Command extends BaseCommand {
usage() {
return 'config [name] [value]';
}
description() {
return _("Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.");
}
options() {
return [
['-v, --verbose', _('Also displays unset and hidden config variables.')],
];
}
async action(args) {
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) value = '********';
if (Setting.isEnum(name)) {
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
} else {
return _('%s = %s', name, value);
}
}
if (!args.name && !args.value) {
let keys = Setting.keys(!verbose, 'cli');
keys.sort();
for (let i = 0; i < keys.length; i++) {
const value = Setting.value(keys[i]);
if (!verbose && !value) continue;
this.stdout(renderKeyValue(keys[i]));
}
app().gui().showConsole();
app().gui().maximizeConsole();
return;
}
if (args.name && !args.value) {
this.stdout(renderKeyValue(args.name));
app().gui().showConsole();
app().gui().maximizeConsole();
return;
}
Setting.setValue(args.name, args.value);
if (args.name == 'locale') {
setLocale(Setting.value('locale'));
app().onLocaleChanged();
}
await Setting.saveAll();
}
}
module.exports = Command;

View File

@@ -1,39 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'cp <note> [notebook]';
}
description() {
return _('Duplicates the notes matching <note> to [notebook]. If no notebook is specified the note is duplicated in the current notebook.');
}
async action(args) {
let folder = null;
if (args['notebook']) {
folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
} else {
folder = app().currentFolder();
}
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args['note']);
if (!notes.length) throw new Error(_('Cannot find "%s".', args['note']));
for (let i = 0; i < notes.length; i++) {
const newNote = await Note.copyToFolder(notes[i].id, folder.id);
Note.updateGeolocation(newNote.id);
}
}
}
module.exports = Command;

View File

@@ -1,41 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'done <note>';
}
description() {
return _('Marks a to-do as done.');
}
static async handleAction(commandInstance, args, isCompleted) {
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
commandInstance.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', args.note));
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
const todoCompleted = !!note.todo_completed;
if (isCompleted === todoCompleted) return;
await Note.save({
id: note.id,
todo_completed: isCompleted ? time.unixMs() : 0,
});
}
async action(args) {
await Command.handleAction(this, args, true);
}
}
module.exports = Command;

View File

@@ -1,44 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
class Command extends BaseCommand {
usage() {
return 'dump';
}
description() {
return 'Dumps the complete database as JSON.';
}
hidden() {
return true;
}
async action(args) {
let items = [];
let folders = await Folder.all();
for (let i = 0; i < folders.length; i++) {
let folder = folders[i];
let notes = await Note.previews(folder.id);
items.push(folder);
items = items.concat(notes);
}
let tags = await Tag.all();
for (let i = 0; i < tags.length; i++) {
tags[i].notes_ = await Tag.noteIds(tags[i].id);
}
items = items.concat(tags);
this.stdout(JSON.stringify(items));
}
}
module.exports = Command;

View File

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

View File

@@ -1,119 +0,0 @@
const fs = require('fs-extra');
const { BaseCommand } = require('./base-command.js');
const { uuid } = require('lib/uuid.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'edit <note>';
}
description() {
return _('Edit note.');
}
async action(args) {
let watcher = null;
let tempFilePath = null;
const onFinishedEditing = async () => {
if (tempFilePath) fs.removeSync(tempFilePath);
}
const textEditorPath = () => {
if (Setting.value('editor')) return Setting.value('editor');
if (process.env.EDITOR) return process.env.EDITOR;
throw new Error(_('No text editor is defined. Please set it using `config editor <editor-path>`'));
}
try {
// -------------------------------------------------------------------------
// Load note or create it if it doesn't exist
// -------------------------------------------------------------------------
let title = args['note'];
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
this.encryptionCheck(note);
if (!note) {
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
if (!ok) return;
note = await Note.save({ title: title, parent_id: app().currentFolder().id });
note = await Note.load(note.id);
}
// -------------------------------------------------------------------------
// Create the file to be edited and prepare the editor program arguments
// -------------------------------------------------------------------------
let editorPath = textEditorPath();
let editorArgs = editorPath.split(' ');
editorPath = editorArgs[0];
editorArgs = editorArgs.splice(1);
const originalContent = await Note.serializeForEdit(note);
tempFilePath = Setting.value('tempDir') + '/' + uuid.create() + '.md';
editorArgs.push(tempFilePath);
await fs.writeFile(tempFilePath, originalContent);
// -------------------------------------------------------------------------
// Start editing the file
// -------------------------------------------------------------------------
this.logger().info('Disabling fullscreen...');
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
await app().gui().forceRender();
const termState = app().gui().termSaveState();
const spawnSync = require('child_process').spawnSync;
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
app().gui().termRestoreState(termState);
app().gui().hideModalOverlay();
app().gui().forceRender();
// -------------------------------------------------------------------------
// Save the note and clean up
// -------------------------------------------------------------------------
const updatedContent = await fs.readFile(tempFilePath, 'utf8');
if (updatedContent !== originalContent) {
let updatedNote = await Note.unserializeForEdit(updatedContent);
updatedNote.id = note.id;
await Note.save(updatedNote);
this.stdout(_('Note has been saved.'));
}
this.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
await onFinishedEditing();
} catch(error) {
await onFinishedEditing();
throw error;
}
}
}
module.exports = Command;

View File

@@ -1,25 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {
usage() {
return 'exit';
}
description() {
return _('Exits the application.');
}
compatibleUis() {
return ['gui'];
}
async action(args) {
await app().exit();
}
}
module.exports = Command;

View File

@@ -1,36 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { Database } = require('lib/database.js');
const { app } = require('./app.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export-sync-status';
}
description() {
return 'Export sync status';
}
hidden() {
return true;
}
async action(args) {
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv';
await fs.writeFileSync(filePath, csv);
this.stdout('Sync status exported to ' + filePath);
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@@ -1,61 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const InteropService = require('lib/services/InteropService.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export <path>';
}
description() {
return _('Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.');
}
options() {
const service = new InteropService();
const formats = service.modules()
.filter(m => m.type === 'exporter')
.map(m => m.format + (m.description ? ' (' + m.description + ')' : ''));
return [
['--format <format>', _('Destination format: %s', formats.join(', '))],
['--note <note>', _('Exports only the given note.')],
['--notebook <notebook>', _('Exports only the given notebook.')],
];
}
async action(args) {
let exportOptions = {};
exportOptions.path = args.path;
exportOptions.format = args.options.format ? args.options.format : 'jex';
if (args.options.note) {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() });
if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note));
exportOptions.sourceNoteIds = notes.map((n) => n.id);
} else if (args.options.notebook) {
const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook);
if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook));
exportOptions.sourceFolderIds = folders.map((n) => n.id);
}
const service = new InteropService();
const result = await service.export(exportOptions);
result.warnings.map((w) => this.stdout(w));
}
}
module.exports = Command;

View File

@@ -1,31 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'geoloc <note>';
}
description() {
return _('Displays a geolocation URL for the note.');
}
async action(args) {
let title = args['note'];
let item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
const url = Note.geolocationUrl(item);
this.stdout(url);
app().gui().showConsole();
}
}
module.exports = Command;

View File

@@ -1,91 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { renderCommandHelp } = require('./help-utils.js');
const { Database } = require('lib/database.js');
const Setting = require('lib/models/Setting.js');
const { wrap } = require('lib/string-utils.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'help [command]';
}
description() {
return _('Displays usage information.');
}
allCommands() {
const commands = app().commands(app().uiType());
let output = [];
for (let n in commands) {
if (!commands.hasOwnProperty(n)) continue;
const command = commands[n];
if (command.hidden()) continue;
if (!command.enabled()) continue;
output.push(command);
}
output.sort((a, b) => a.name() < b.name() ? -1 : +1);
return output;
}
async action(args) {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts' || args.command === 'keymap') {
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplinapp.org/terminal/#shortcuts'));
this.stdout('');
if (app().gui().isDummy()) {
throw new Error(_('Shortcuts are not available in CLI mode.'));
}
const keymap = app().gui().keymap();
let rows = [];
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);
} else if (args.command === 'all') {
const commands = this.allCommands();
const output = commands.map((c) => renderCommandHelp(c));
this.stdout(output.join('\n\n'));
} else if (args.command) {
const command = app().findCommandByName(args['command']);
if (!command) throw new Error(_('Cannot find "%s".', args.command));
this.stdout(renderCommandHelp(command, stdoutWidth));
} else {
const commandNames = this.allCommands().map((a) => a.name());
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
this.stdout('');
this.stdout(_('The possible commands are:'));
this.stdout('');
this.stdout(commandNames.join(', '));
this.stdout('');
this.stdout(_('In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
this.stdout('');
this.stdout(_('To move from one pane to another, press Tab or Shift+Tab.'));
this.stdout(_('Use the arrows and page up/down to scroll the lists and text areas (including this console).'));
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 list of keyboard shortcuts and config options, type `help keymap`'));
}
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@@ -1,68 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const { importEnex } = require('lib/import-enex');
const { filename, basename } = require('lib/path-utils.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'import-enex <file> [notebook]';
}
description() {
return _('Imports an Evernote notebook file (.enex file).');
}
options() {
return [
['-f, --force', _('Do not ask for confirmation.')],
];
}
async action(args) {
let filePath = args.file;
let folder = null;
let folderTitle = args['notebook'];
let force = args.options.force === true;
if (!folderTitle) folderTitle = filename(filePath);
folder = await Folder.loadByField('title', folderTitle);
const msg = folder ? _('File "%s" will be imported into existing notebook "%s". Continue?', basename(filePath), folderTitle) : _('New notebook "%s" will be created and file "%s" will be imported into it. Continue?', folderTitle, basename(filePath));
const ok = force ? true : await this.prompt(msg);
if (!ok) return;
let lastProgress = '';
let options = {
onProgress: (progressState) => {
let line = [];
line.push(_('Found: %d.', progressState.loaded));
line.push(_('Created: %d.', progressState.created));
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
lastProgress = line.join(' ');
cliUtils.redraw(lastProgress);
},
onError: (error) => {
let s = error.trace ? error.trace : error.toString();
this.stdout(s);
},
}
folder = !folder ? await Folder.save({ title: folderTitle }) : folder;
app().gui().showConsole();
this.stdout(_('Importing notes...'));
await importEnex(folder.id, filePath, options);
cliUtils.redrawDone();
this.stdout(_('The notes have been imported: %s', lastProgress));
}
}
module.exports = Command;

View File

@@ -1,75 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const InteropService = require('lib/services/InteropService.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const { filename, basename, fileExtension } = require('lib/path-utils.js');
const { importEnex } = require('lib/import-enex');
const { cliUtils } = require('./cli-utils.js');
const { reg } = require('lib/registry.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'import <path> [notebook]';
}
description() {
return _('Imports data into Joplin.');
}
options() {
const service = new InteropService();
const formats = service.modules().filter(m => m.type === 'importer').map(m => m.format);
return [
['--format <format>', _('Source format: %s', (['auto'].concat(formats)).join(', '))],
['-f, --force', _('Do not ask for confirmation.')],
];
}
async action(args) {
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
if (args.notebook && !folder) throw new Error(_('Cannot find "%s".', args.notebook));
const importOptions = {};
importOptions.path = args.path;
importOptions.format = args.options.format ? args.options.format : 'auto';
importOptions.destinationFolderId = folder ? folder.id : null;
let lastProgress = '';
// onProgress/onError supported by Enex import only
importOptions.onProgress = (progressState) => {
let line = [];
line.push(_('Found: %d.', progressState.loaded));
line.push(_('Created: %d.', progressState.created));
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
lastProgress = line.join(' ');
cliUtils.redraw(lastProgress);
};
importOptions.onError = (error) => {
let s = error.trace ? error.trace : error.toString();
this.stdout(s);
};
app().gui().showConsole();
this.stdout(_('Importing notes...'));
const service = new InteropService();
const result = await service.import(importOptions);
result.warnings.map((w) => this.stdout(w));
cliUtils.redrawDone();
if (lastProgress) this.stdout(_('The notes have been imported: %s', lastProgress));
}
}
module.exports = Command;

View File

@@ -1,126 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Setting = require('lib/models/Setting.js');
const Note = require('lib/models/Note.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'ls [note-pattern]';
}
description() {
return _('Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.');
}
enabled() {
return false;
}
options() {
return [
['-n, --limit <num>', _('Displays only the first top <num> notes.')],
['-s, --sort <field>', _('Sorts the item by <field> (eg. title, updated_time, created_time).')],
['-r, --reverse', _('Reverses the sorting order.')],
['-t, --type <type>', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')],
['-f, --format <format>', _('Either "text" or "json"')],
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
}
async action(args) {
let pattern = args['note-pattern'];
let items = [];
let options = args.options;
let queryOptions = {};
if (options.limit) queryOptions.limit = options.limit;
if (options.sort) {
queryOptions.orderBy = options.sort;
queryOptions.orderByDir = 'ASC';
}
if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir == 'ASC' ? 'DESC' : 'ASC';
queryOptions.caseInsensitive = true;
if (options.type) {
queryOptions.itemTypes = [];
if (options.type.indexOf('n') >= 0) queryOptions.itemTypes.push('note');
if (options.type.indexOf('t') >= 0) queryOptions.itemTypes.push('todo');
}
if (pattern) queryOptions.titlePattern = pattern;
queryOptions.uncompletedTodosOnTop = Setting.value('uncompletedTodosOnTop');
let modelType = null;
if (pattern == '/' || !app().currentFolder()) {
queryOptions.includeConflictFolder = true;
items = await Folder.all(queryOptions);
modelType = Folder.modelType();
} else {
if (!app().currentFolder()) throw new Error(_('Please select a notebook first.'));
items = await Note.previews(app().currentFolder().id, queryOptions);
modelType = Note.modelType();
}
if (options.format && options.format == 'json') {
this.stdout(JSON.stringify(items));
} else {
let hasTodos = false;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.is_todo) {
hasTodos = true;
break;
}
}
let seenTitles = [];
let rows = [];
let shortIdShown = false;
for (let i = 0; i < items.length; i++) {
let item = items[i];
let row = [];
if (options.long) {
row.push(BaseModel.shortId(item.id));
shortIdShown = true;
if (modelType == Folder.modelType()) {
row.push(await Folder.noteCount(item.id));
}
row.push(time.formatMsToLocal(item.user_updated_time));
}
let title = item.title;
if (!shortIdShown && (seenTitles.indexOf(item.title) >= 0 || !item.title)) {
title += ' (' + BaseModel.shortId(item.id) + ')';
} else {
seenTitles.push(item.title);
}
if (hasTodos) {
if (item.is_todo) {
row.push(sprintf('[%s]', !!item.todo_completed ? 'X' : ' '));
} else {
row.push(' ');
}
}
row.push(title);
rows.push(row);
}
cliUtils.printArray(this.stdout.bind(this), rows);
}
}
}
module.exports = Command;

View File

@@ -1,24 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Folder = require('lib/models/Folder.js');
const { reg } = require('lib/registry.js');
class Command extends BaseCommand {
usage() {
return 'mkbook <new-notebook>';
}
description() {
return _('Creates a new notebook.');
}
async action(args) {
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
app().switchCurrentFolder(folder);
}
}
module.exports = Command;

View File

@@ -1,32 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mknote <new-note>';
}
description() {
return _('Creates a new note.');
}
async action(args) {
if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.'));
let note = {
title: args['new-note'],
parent_id: app().currentFolder().id,
};
note = await Note.save(note);
Note.updateGeolocation(note.id);
app().switchCurrentFolder(app().currentFolder());
}
}
module.exports = Command;

View File

@@ -1,33 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mktodo <new-todo>';
}
description() {
return _('Creates a new to-do.');
}
async action(args) {
if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.'));
let note = {
title: args['new-todo'],
parent_id: app().currentFolder().id,
is_todo: 1,
};
note = await Note.save(note);
Note.updateGeolocation(note.id);
app().switchCurrentFolder(app().currentFolder());
}
}
module.exports = Command;

View File

@@ -1,35 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'mv <note> [notebook]';
}
description() {
return _('Moves the notes matching <note> to [notebook].');
}
async action(args) {
const pattern = args['note'];
const destination = args['notebook'];
const folder = await Folder.loadByField('title', destination);
if (!folder) throw new Error(_('Cannot find "%s".', destination));
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
for (let i = 0; i < notes.length; i++) {
await Note.moveToFolder(notes[i].id, folder.id);
}
}
}
module.exports = Command;

View File

@@ -1,41 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseModel = require('lib/BaseModel.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
class Command extends BaseCommand {
usage() {
return 'ren <item> <name>';
}
description() {
return _('Renames the given <item> (note or notebook) to <name>.');
}
async action(args) {
const pattern = args['item'];
const name = args['name'];
const item = await app().loadItem('folderOrNote', pattern);
this.encryptionCheck(item);
if (!item) throw new Error(_('Cannot find "%s".', pattern));
const newItem = {
id: item.id,
title: name,
type_: item.type_,
};
if (item.type_ === BaseModel.TYPE_FOLDER) {
await Folder.save(newItem);
} else {
await Note.save(newItem);
}
}
}
module.exports = Command;

View File

@@ -1,40 +0,0 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { cliUtils } = require('./cli-utils.js');
class Command extends BaseCommand {
usage() {
return 'rmbook <notebook>';
}
description() {
return _('Deletes the given notebook.');
}
options() {
return [
['-f, --force', _('Deletes the notebook without asking for confirmation.')],
];
}
async action(args) {
const pattern = args['notebook'];
const force = args.options && args.options.force === true;
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
if (!ok) return;
await Folder.delete(folder.id);
}
}
module.exports = Command;

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