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

Compare commits

..

1512 Commits

Author SHA1 Message Date
Laurent Cozic
a82f8c7dd0 CLI v1.0.126 2019-05-13 00:42:16 +01:00
Helmut K. C. Tessarek
cde079e44e Clipper release v1.0.14 2019-05-12 17:52:20 -04:00
Laurent Cozic
f8d20b61ea Update website 2019-05-12 16:18:30 +01:00
Laurent Cozic
d279435502 Android release v1.0.251 2019-05-12 16:16:39 +01:00
Laurent Cozic
eb2065128e Electron release v1.0.151 2019-05-12 16:04:01 +01:00
Laurent Cozic
10e81aa476 Trying to fix iOS release issue 2019-05-12 16:03:22 +01:00
Laurent Cozic
3e808f05fd Improved logic to set resource file size 2019-05-12 15:53:42 +01:00
Laurent Cozic
e57bfad9b1 Android release v1.0.248 2019-05-12 11:50:21 +01:00
Laurent Cozic
5f344f07d4 Electron release v1.0.150 2019-05-12 11:41:35 +01:00
Laurent Cozic
e1b7b64e1b All: Make sure resource filesize is set in all cases 2019-05-12 11:41:07 +01:00
Laurent Cozic
ed3970be81 CLI: Fix: Do not resize images if they are already below the max dimensions 2019-05-12 11:38:33 +01:00
Laurent Cozic
c27861d40f Electron release v1.0.149 2019-05-12 01:22:02 +01:00
Laurent Cozic
565dfba8c9 All: Fixes #371 (sort of): Allow resources greater than 10 MB but they won't be synced on mobile 2019-05-12 01:15:52 +01:00
Laurent Cozic
553a26eb63 Desktop, CLI: Added option to disable creation of welcome items 2019-05-12 01:10:46 +01:00
Laurent Cozic
9c85bc2cd1 All: Save size of a resource to the database; and added mechanism to run non-database migrations 2019-05-11 17:55:40 +01:00
Laurent Cozic
e96bc9c48a All: Allow specifying the log level of a log target 2019-05-11 17:53:56 +01:00
Laurent Cozic
0d036d8183 Fixed regression following fix for #1425 2019-05-11 17:35:39 +01:00
Laurent Cozic
e5f2a7f2f5 Fixed regression caused by #1472 2019-05-11 17:34:45 +01:00
Laurent Cozic
016ce3dd61 Desktop: Resolves #1502: Improved note deletion dialog 2019-05-11 13:36:44 +01:00
Laurent Cozic
afb375955e Fixed regressions following fix for #1425 2019-05-11 12:08:28 +01:00
Laurent Cozic
b702b0b40c Desktop: Fixes #1425: Improved handling of images when using external editor, so that it works in Atom, VSCode and Typora 2019-05-11 11:46:13 +01:00
Laurent Cozic
91ecab51c5 Clipper: Fixed: Added Chrome workaround to prevent it from posting the same note twice 2019-05-11 11:18:09 +01:00
Laurent Cozic
7628506926 Merge branch 'master' of github.com:laurent22/joplin 2019-05-11 11:13:23 +01:00
Laurent Cozic
4e7f7c0c9c Clipper: Fixes #1510: Fixed display of some images in preview. Also display images at correct size inside preview. 2019-05-11 11:13:13 +01:00
Laurent Cozic
863f5bcf18 Desktop, Mobile: Fixed: Some images were not being displayed 2019-05-11 09:49:56 +01:00
Laurent Cozic
dccd489fcc Clipper: Fix: Fix handling of tables with empty headers 2019-05-11 09:23:31 +01:00
Helmut K. C. Tessarek
333c3f6369 added .gitignore to /Tools 2019-05-10 20:19:42 -04:00
Helmut K. C. Tessarek
808413d0bf remove section self-signed certs from FAQ for now - no Android rel available that has this feature yet 2019-05-10 03:19:47 -04:00
Helmut K. C. Tessarek
440be3d920 remove unintentionally committed files again 2019-05-09 23:01:49 -04:00
Helmut K. C. Tessarek
9b27a4f601 Update website 2019-05-09 22:58:56 -04:00
Don Bowman
6e36ca32b4 CLI: Fix: Bump sqlite3 to v4.0.7 for node12 support (#1508) 2019-05-10 01:18:04 +01:00
simonsan
85a9c303f2 Changed download links to v1.0.145 (#1507) 2019-05-10 01:17:08 +01:00
Krešimir Klas
7e9972d99f Android: Resolves #680: New: Allow self-signed certificates (#1466)
* Allow User-added CAs in Android

This will enable connecting to servers with self-signed certificates on
android as per issue #680.

Implemented as per:
- https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
- https://github.com/facebook/react-native/issues/20488

* Allow User-added CAs in Android

This will enable connecting to servers with self-signed certificates on
android as per issue #680.

Implemented as per:
- https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
- https://github.com/facebook/react-native/issues/20488
2019-05-10 01:15:13 +01:00
Laurent Cozic
771975cd35 Clipper: Fixes #1462: Allow importing images from local file with file:// URLs 2019-05-10 01:06:06 +01:00
Laurent Cozic
356f8e580b Clipper: Improved: Updated Readability library to improve Simplified Page clipping 2019-05-10 00:05:23 +01:00
Laurent Cozic
68268cb35d Merge branch 'master' of github.com:laurent22/joplin 2019-05-09 23:46:41 +01:00
Laurent Cozic
8e58ed12af Clipper: Resolves #1379: Improved: Display warning icon when a page might not render well in simplified mode 2019-05-09 23:41:52 +01:00
Laurent Cozic
beb428b246 Android release v1.0.246 2019-05-08 00:54:46 +01:00
Laurent Cozic
4d81caff0b Electron release v1.0.148 2019-05-08 00:52:20 +01:00
Laurent Cozic
78372c9bac All: Improved: Make sure a revision is saved a note has not been modified for over a week 2019-05-08 00:51:56 +01:00
Laurent Cozic
a4db1bc671 All: Improved: Do not save a revision if there is already a recent one that exists 2019-05-08 00:10:36 +01:00
Laurent Cozic
8ea1c373ed Fixed position of config button 2019-05-07 23:50:12 +01:00
Laurent Cozic
8ef27dfcdc Merge branch 'master' of github.com:laurent22/joplin 2019-05-07 23:43:40 +01:00
Laurent Cozic
23e43c7bc1 Desktop: Fix: Fixed note history sort order. 2019-05-07 23:42:46 +01:00
Helmut K. C. Tessarek
edb8f4c79f Merge pull request #1503 from ruzaq/master
CLI: CZech translation updated
2019-05-07 17:46:39 -04:00
Ruzicka Pavel
e115fa4bb3 CZech translation updated 2019-05-07 23:30:18 +02:00
Laurent Cozic
52a2daddbf All: Improved: Make sure user timestamp is preserved with revision information 2019-05-07 22:15:47 +01:00
Laurent Cozic
c400142996 All: Fix: Make sure a revision is not empty before saving it 2019-05-07 20:46:58 +01:00
Laurent Cozic
219171a18c Remove dependency to git2json and improved handling of new commit messages 2019-05-06 22:18:17 +01:00
Laurent Cozic
d7d573d9dd Android release v1.0.245 2019-05-06 21:51:42 +01:00
Laurent Cozic
c4b17f8919 Electron release v1.0.147 2019-05-06 21:48:50 +01:00
Laurent Cozic
da2f4b96c7 Electron release v1.0.146 2019-05-06 21:48:37 +01:00
Laurent Cozic
08af9de190 All: Resolves #712: New: Support for note history (#1415)
* Started revisions support

* More rev changes

* More rev changes

* More revs changes

* Fixed deletion algorithm

* More tests and moved updated time to separate field

* Display info when restoring note

* Better handling of existing notes

* wip

* Further improvements and fixed tests

* Better handling of changes created via sync

* Enable chokidar again

* Testing special case

* Further improved logic to handle notes that existed before the revision service

* Added tests

* Better handling of encrypted revisions

* Improved handling of deleted note revisions by moving logic to collectRevision

* Improved handling of old notes by moving logic to collectRevision()

* Handle case when deleting revisions while one is still encrypted

* UI tweaks

* Added revision service to mobile app

* Fixed config screens on mobile and desktop

* Enabled revisions on CLI app
2019-05-06 21:35:29 +01:00
Laurent Cozic
9e2982992a Merge branch 'master' of github.com:laurent22/joplin 2019-05-06 21:31:45 +01:00
Laurent Cozic
c03ac5c5f1 Make sure Appveyor only build tags 2019-05-06 21:31:37 +01:00
Luis Orozco
5934f2f08e Desktop: Fixes #355: Resets the undo manager when creating new notes (#1495) 2019-05-06 21:30:04 +01:00
Caleb John
f136f40fdc Doc: Update readme to warn about restarting the editor for userstyle (#1487) 2019-05-06 21:26:34 +01:00
Luis Orozco
d213e4ab57 All: Fixed: Prevents notes with no title to break after synchronize (#1472)
Tests to confirm serialize/unserialize don't change body and title

check if item title exists, otherwise display default title.

added test checking serializing/unserializing Folders don't modify data
2019-05-06 21:25:14 +01:00
Laurent Cozic
782aae4ddf Doc: Better handling of platform in changelog autogenerate 2019-05-03 15:02:32 +00:00
Laurent Cozic
4f47bd7bcd Android release v1.0.244 2019-05-03 00:22:11 +01:00
Laurent Cozic
4f76946140 Electron release v1.0.145 2019-05-03 00:20:01 +01:00
Laurent Cozic
aa60923cbd Doc: Better handling of platforms in changelog generation 2019-05-03 00:19:42 +01:00
doc75
7670ce32b1 removing sh in Linux installation command line as this is unnecessary (and failing on Ubuntu) (#1484) 2019-05-02 23:58:38 +01:00
Laurent Cozic
0b98632336 Update website 2019-05-02 23:54:52 +01:00
Laurent Cozic
49c998de83 Doc: Auto-generate anchors 2019-05-02 23:54:31 +01:00
Helmut K. C. Tessarek
da69d6b2c9 Desktop: Fix: Update chokidar to fix blank screen when returning from external editor (#1479) 2019-05-02 16:00:17 +01:00
Michael Schneider
a757aefce0 Remove separator between New Notebook and Close Window in File Menu (#1483) 2019-05-02 15:55:48 +01:00
Laurent Cozic
b2129cb8c4 Desktop, CLI: Fixes #1476: Import lists and sub-lists from Enex files with correct indentation 2019-05-01 18:06:37 +01:00
Laurent Cozic
27f14c175f Electron release v1.0.144 2019-04-30 23:42:42 +01:00
Laurent Cozic
aad49c520b All: Improved: Display better error message when trying to sync with a new sync target from an old version of Joplin 2019-04-30 23:42:06 +01:00
Michael Schneider
af794a16d6 Desktop (macOS): Add macOS "Close Window" menu item and add name to Quit menu (#1434)
* Add Close Window to macOS file menu

* Add Joplin to Quit menu item

On macOS the application name appears usually within the Quit menu item.

* Use performClose: selector for Close Window

* Revert Quit with name and add Quit string to translations

* Move Quit translation to joplin.pot

* Remove Quit string
2019-04-30 21:38:20 +01:00
Laurent Cozic
ca7266cd69 Fixing images 2019-04-30 19:04:24 +01:00
Laurent Cozic
fb758afc81 Update website 2019-04-30 18:58:35 +01:00
Laurent Cozic
b806f0da49 Removed dependency to unsafe and buggy marked 2019-04-30 18:58:19 +01:00
Laurent Cozic
4e3b1f3e13 Merge branch 'master' of github.com:laurent22/joplin 2019-04-30 18:29:46 +01:00
Laurent Cozic
475467c41c Update website 2019-04-30 18:29:43 +01:00
Caleb John
28e5039873 Update Joplin_install_and_update from #1422 and #1473 (#1475) 2019-04-30 18:28:36 +01:00
Laurent Cozic
1efc6e6151 Merge branch 'master' of github.com:laurent22/joplin 2019-04-30 18:27:08 +01:00
Laurent Cozic
e280a02643 Update doc 2019-04-30 18:26:58 +01:00
Christian Moritz
788dc42684 CLI: Improved: Update sharp (for node 12 compatibility) (#1471) 2019-04-30 17:32:51 +01:00
Laurent Cozic
01f2759a62 Added CLI changelog 2019-04-29 18:39:43 +01:00
Laurent Cozic
9419e3af9c CLI v1.0.125 2019-04-29 18:36:32 +01:00
Laurent Cozic
6d220005cc All: Fixes #1353: Remove message "Processing a path that has already been done" as this is not an error 2019-04-29 18:27:32 +01:00
Laurent Cozic
6d68e61bbd Exclude reverted commits from changelog 2019-04-29 07:42:40 +01:00
Laurent Cozic
29582623b0 Revert "Desktop: Improved: Removed gaps between note list and vertical resizers (#1464)"
This reverts commit d6e59c5238.
2019-04-29 07:28:16 +01:00
Laurent Cozic
4571e7853a Improved auto-generation of changelog 2019-04-28 15:09:07 +01:00
Luis Orozco
d6e59c5238 Desktop: Improved: Removed gaps between note list and vertical resizers (#1464) 2019-04-28 14:23:17 +01:00
Luis Orozco
6335cbedb8 Desktop: Improved: UI updates to sidebar and header, changing icon sizes and animations (#1463)
Added animation to icon in synchronize button

Moved sync report above button, which prevents the sync button from moving from its place when the report has text.

Added animation to icon in Toggle Sidebar button, using the css transition property.

Reduced font size for text and icons in header and sidebar

Changed theme color2 from white to a very light grey. It is barely
noticeable, but reduces contrast a bit, improving readability.
2019-04-28 14:20:18 +01:00
Laurent Cozic
f3344ce05d Removed "Demo" word from screenshot to comply with App Store 2019-04-28 13:11:15 +01:00
Laurent Cozic
155d38d24a Update website 2019-04-26 19:06:17 +01:00
Laurent Cozic
412f6d8316 Fixing Arabic flag 2019-04-26 18:58:40 +01:00
Laurent Cozic
d0f3ed80e0 Added new required iOS screenshots 2019-04-26 18:37:30 +01:00
Helmut K. C. Tessarek
b86f3b74bd build-translation.js: hack to show en_US as 100% (#1448) 2019-04-26 18:36:12 +01:00
Laurent Cozic
60054d1d8b ios-v10.0.31 2019-04-26 08:52:59 +01:00
Laurent Cozic
c40c6428d7 Merge branch 'master' of github.com:laurent22/joplin 2019-04-26 08:30:51 +01:00
Laurent Cozic
e708ecccee Updated FAQ 2019-04-26 08:30:40 +01:00
Helmut K. C. Tessarek
04f991d3bf Update website 2019-04-23 16:10:41 -04:00
Helmut K. C. Tessarek
d5d7368ba0 Merge pull request #1446 from Fvbor/patch-1
Readme: Change Download links from 1.0.142 to 1.0.143
2019-04-23 16:09:16 -04:00
Hagen Tasche
abff929d4e Change Download links from 1.0.142 to 1.0.143 2019-04-23 09:09:11 +02:00
Laurent Cozic
49edc82594 Tools: Allow auto-generating changelog from commit messages 2019-04-22 18:02:45 +00:00
Helmut K. C. Tessarek
7dd7d0ec17 Merge pull request #1439 from tessus/localization-de_DE
update localization de_DE.po
2019-04-22 00:01:34 -04:00
Helmut K. C. Tessarek
61aaf64f95 update localization de_DE.po 2019-04-22 00:00:54 -04:00
Helmut K. C. Tessarek
da35785951 Update website 2019-04-21 14:55:52 -04:00
Helmut K. C. Tessarek
e394034678 restore the content of API.md 2019-04-21 14:49:12 -04:00
Laurent Cozic
edf002ab32 Android release v1.0.243 2019-04-21 14:58:33 +01:00
Laurent Cozic
50f2076981 Electron release v1.0.143 2019-04-21 14:56:13 +01:00
Laurent Cozic
a9ae78bcde Updated translations 2019-04-21 14:55:12 +01:00
Laurent Cozic
0d7f9a2ab3 Merge branch 'master' of github.com:laurent22/joplin 2019-04-21 13:56:01 +01:00
Laurent Cozic
a5ee120281 All: Fixes #1433: Some resources could incorrectly be deleted even though they are still present in a note. Also added additional verifications before deleting a resource. 2019-04-21 13:49:40 +01:00
Helmut K. C. Tessarek
12ebf44e22 Desktop: add option to use soft breaks for markdown rendering (#1408)
* add option to use soft breaks for markdown rendering

* oops, I didn't use the British spelling

* moved setting to section Plugins

* change text -> Enable soft breaks
2019-04-21 10:03:10 +01:00
Laurent Cozic
220f5d0967 Desktop: Fixes #1425 (probably): Fix display of images when using VSCode as external editor 2019-04-20 21:12:19 +01:00
Laurent Cozic
4ef05272c4 Clipper: Fixes #1417: Sort the folders in the same order as the desktop app 2019-04-20 19:29:23 +01:00
Laurent Cozic
c3262aa5f8 Desktop: Allow CtrlCmd+G shortcut from text editor 2019-04-20 19:07:13 +01:00
Laurent Cozic
42119c8f42 All: Fixes #1427: Support checkoxes behind bullets 2019-04-20 19:00:22 +01:00
Laurent Cozic
27cce03968 Updated translations 2019-04-20 12:26:34 +01:00
Laurent Cozic
776aba1e49 Merge branch 'master' of github.com:laurent22/joplin 2019-04-20 12:23:55 +01:00
Laurent Cozic
9356841cfc Desktop: Fixes #423: Make sure links are clickable when exporting to PDF 2019-04-20 12:23:05 +01:00
Helmut K. C. Tessarek
7fc233e808 Desktop: change shortcuts for 'Print' and 'Goto Anything' (#1420)
Print            Command/Control + P
Goto Anything    Command/Control + G

closes #1413
2019-04-20 12:02:43 +01:00
Helmut K. C. Tessarek
7b2eac3abd add localization for English (United States) en_US (#1391) 2019-04-20 12:01:53 +01:00
Laurent Cozic
93323deea5 Desktop: Fix: Updated Electron and Chokidar to try to fix external editor crashing app 2019-04-20 12:00:42 +01:00
Laurent Cozic
e8fa399e9e Update website 2019-04-18 15:26:13 +01:00
Laurent Cozic
a974eb5d9f Fixing links 2019-04-18 15:11:56 +01:00
Laurent Cozic
6a3f04274d Update website 2019-04-18 15:00:32 +01:00
Laurent Cozic
f8e1395087 joplin.cozic.net => joplinapp.org 2019-04-18 14:59:17 +01:00
Laurent Cozic
82b5af51e5 Update CNAME 2019-04-18 14:06:15 +01:00
Laurent Cozic
cf40c14a86 All: Fixes #1405: Handle invalid resource tags that contain no data when importing ENEX 2019-04-12 01:43:15 +01:00
Laurent Cozic
be2b2b7836 Merge branch 'master' of github.com:laurent22/joplin 2019-04-04 08:14:36 +01:00
Laurent Cozic
0cebae8032 All: Allow longer folder paths 2019-04-04 08:01:16 +01:00
Laurent Cozic
6ebc77cbba Desktop: Fix: Goto Anything results were displayed lowercase 2019-04-04 07:53:20 +01:00
Tim Kilåker
542a5e88b7 All: 1383 Clear selected Notes when switching Notebook (#1387) 2019-04-04 07:48:33 +01:00
Laurent Cozic
72b36522e8 All: Improved support for Japanese, Chinese, Korean search queries (also applies to Goto Anything) 2019-04-03 07:46:41 +01:00
Laurent Cozic
252d937405 Update website 2019-04-02 17:46:39 +01:00
Laurent Cozic
a73b0309b9 Electron release v1.0.142 2019-04-02 17:27:52 +01:00
Laurent Cozic
c22283e799 Electron release v1.0.141 2019-04-02 17:27:39 +01:00
Laurent Cozic
1e51ab4a59 Updated translations 2019-04-02 17:27:19 +01:00
Caleb John
0c2f2667d3 All: Allow toggling markdown plugins and added several new plugins (#1347)
* Initial test of enabling plugins

* Added support for toggle-able plugins
- Also adds some new plugins

* Add instructions on adding toggle-plugins

* Fix subtle anchor bug
- webview was moving itself when scrolling to bottom anchors

* Moves the webview hack so that it only applies to anchors

* Add plugin descriptions to the README, also removed mermaid from README

* rename plugin.* to markdown.plugin.* to be more forward compatible
2019-04-02 17:14:48 +01:00
Laurent Cozic
496c9ddb91 Fixed tests to match new behaviour 2019-04-02 00:22:04 +01:00
Laurent Cozic
5ad0b2eed9 Clean up 2019-04-02 00:12:38 +01:00
Laurent Cozic
577d62e783 Updated translations 2019-04-02 00:11:40 +01:00
Laurent Cozic
dcb73c9916 Desktop: Fixed menu logic for non-macOS 2019-04-01 19:51:47 +00:00
Laurent Cozic
6b2910c3c7 Desktop: Added Goto Anything dialog and implemented basic plugin system 2019-04-01 19:43:13 +00:00
Helmut K. C. Tessarek
db04906416 Desktop: macOS: make the menu more like a macOS menu (#1348)
* macOS: make the menu more like a macOS menu

* remove duplcated code even more

* yep, I forgot to localize the new menu item

* more de-duplication, create File menu on macOS
2019-04-01 10:04:34 +01:00
s1nceri7y
54fceeb07d Update README.md (#1375)
added links to browser extensions
2019-03-31 18:27:36 +01:00
RaphaelKimmig
fa32678645 improve search bar text alignment (#1377) 2019-03-31 18:26:47 +01:00
RaphaelKimmig
ee1df1a396 fix sub pixel rendering for desktop (#1378) 2019-03-31 18:25:13 +01:00
Laurent Cozic
729be8767c Clipper release v1.0.13 2019-03-31 12:15:24 +01:00
Helmut K. C. Tessarek
8471f0d86d Merge pull request #1374 from s1nceri7y/patch-1
Update ru_RU.po
2019-03-30 16:34:43 -04:00
s1nceri7y
390b818d71 Update ru_RU.po
translation adjustments
2019-03-30 10:14:44 +00:00
Laurent Cozic
1a1c190ea3 Clipper: Move Confirm button up 2019-03-29 10:11:27 +00:00
Laurent Cozic
40d82b80f1 Clipper release v1.0.12 2019-03-29 09:45:19 +00:00
Laurent Cozic
7647ecbbc7 Clipper: Do not inline scripts to prevent CSP error 2019-03-29 09:44:56 +00:00
Laurent Cozic
b2a5cf9dd0 Clipper release v1.0.10 2019-03-29 09:06:55 +00:00
Laurent Cozic
cbf3ab2ec2 Merge branch 'master' of github.com:laurent22/joplin 2019-03-29 09:04:10 +00:00
Laurent Cozic
c4a37ff0ba Clipper: Better handling of error on tabs where extension is not available 2019-03-29 09:03:21 +00:00
Laurent Cozic
bdc7ea4346 All: Display warning when changing dir for filesystem sync 2019-03-29 08:01:58 +00:00
Tekki
d4c4b9b10a Replace curl with wget in installation script. (#1368) 2019-03-28 21:19:07 +00:00
Caleb John
4b9105edff Set blockScrolling to Infinty (fixes error message) (#1363) 2019-03-28 21:18:53 +00:00
Caleb John
c0980a5a9e prevent appimage integration prompt when joplin.desktop already exists (#1337) 2019-03-28 20:49:44 +00:00
Laurent Cozic
272055fc1d Updated translations 2019-03-28 17:06:06 +00:00
Laurent Cozic
cbb1851b12 Updated translations 2019-03-28 17:05:08 +00:00
Helmut K. C. Tessarek
45d758d52e Merge pull request #1362 from Shaxine/patch-1
Update pt_BR.po
2019-03-24 17:53:02 -04:00
Helmut K. C. Tessarek
49936ef095 update download links, stats, website 2019-03-24 17:50:27 -04:00
António Pereira
986d4be601 Update pt_BR.po
Keep command as intended.
2019-03-24 12:43:47 +00:00
Mats Estensen
b6ad9719ad Translation: complete norwegian, remove fuzzy entries (#1333)
* complete norwegian, remove fuzzy entries

* minor typos
2019-03-17 01:19:31 -04:00
Laurent Cozic
96a1546da1 Desktop: Improved search - when clearing search, stay on current item. When clicking on notebook name, jump to note within notebook. Improved toolbar layout. 2019-03-15 21:57:58 +00:00
Laurent Cozic
6884dd2b9e Cleaned up MdToHtml style 2019-03-13 23:03:55 +00:00
Laurent Cozic
9c027e59c4 Android release v1.0.242 2019-03-13 22:52:24 +00:00
Laurent Cozic
9e16ff3644 Mobile: Moved tags to separate screen to avoid slow down when having many tags 2019-03-13 22:42:16 +00:00
Laurent Cozic
4000cb5d1c All: Fixes #1325: Fixed nested checkbox indentation 2019-03-12 22:24:44 +00:00
Laurent Cozic
1030b412ff All: Fixes #1326: Restored inline code styling 2019-03-12 22:07:02 +00:00
Laurent Cozic
54f0fbcf6b Desktop: Fixes #1329: Could not edit created and updated time anymroe 2019-03-12 21:52:23 +00:00
Laurent Cozic
1602182085 Desktop: Fixes #1334 (maybe): Upgraded chokidar which it seems was randomly making Electron 4 crash (maybe due to fsevent package) 2019-03-12 21:41:13 +00:00
Laurent Cozic
20bb1238c5 CLI v1.0.124 2019-03-11 19:37:52 +00:00
Laurent Cozic
68fbe8125e Desktop: Fixed: The side bar was being refreshed too frequently. Fixed: Order of notebooks with sub-notebooks was sometimes incorrect when sorting by last updated time. 2019-03-10 21:16:05 +00:00
Laurent Cozic
23e6e6e69d Android release v1.0.241 2019-03-10 20:35:01 +00:00
Laurent Cozic
ade5af2559 Electron release v1.0.140 2019-03-10 20:32:43 +00:00
Laurent Cozic
0a993dc012 Update translations 2019-03-10 20:32:28 +00:00
xcffl
4baa46507f update zh-CN translation (#1321) 2019-03-10 20:31:39 +00:00
Laurent Cozic
0d8878abd3 Mobile: For now, make notebooks sorted by title 2019-03-10 20:30:31 +00:00
Helmut K. C. Tessarek
f962084591 Merge pull request #1318 from Ouvill/feature/update_jp_translation
update jp translation
2019-03-10 00:48:35 -05:00
Helmut K. C. Tessarek
921b45286b Merge pull request #1316 from tessus/localization-de_DE
Update de_DE.po
2019-03-10 00:48:13 -05:00
Ouvill
301bfed05e update jp translation 2019-03-09 12:56:55 +09:00
Helmut K. C. Tessarek
62e7d6fa86 Update de_DE.po 2019-03-08 13:51:08 -05:00
Laurent Cozic
7e34cd4452 Android release v1.0.240 2019-03-08 17:43:01 +00:00
Laurent Cozic
b35cb9a7ab Electron release v1.0.139 2019-03-08 17:40:11 +00:00
Laurent Cozic
18b836525c Updated translations 2019-03-08 17:39:26 +00:00
Caleb John
e34e49b88d Add joplin.desktop file for xfce and mate desktops (#1304) 2019-03-08 17:33:15 +00:00
Renato Rosa
5bf879c2d9 Update pt_BR.po (#1303)
Minor typo fixed.
2019-03-08 17:33:01 +00:00
Joel Simpson
61d6309c0e Desktop: Adding progress bar and text changes to install script. (#1294) 2019-03-08 17:32:20 +00:00
Helmut K. C. Tessarek
17c9c0f9ef Mobile: version info changes (#1290)
* update the version info string for mobiles to be consistent with the desktop

Joplin vX.Y.Z -> Joplin X.Y.Z
hardcoded the string. Joplin is a product name, no need for translation.

* show version as X.Y.Z in update dialog

* update .po files so that the previous commit won't break translations

* tag -> Schlagwort
2019-03-08 17:31:48 +00:00
Laurent Cozic
9bd62fd3d4 All: Refresh folder order when a new folder is added or modified 2019-03-08 17:25:32 +00:00
Laurent Cozic
de73d4baa7 Minor issue 2019-03-08 17:19:06 +00:00
Laurent Cozic
379ff5163b Merge branch 'master' of github.com:laurent22/joplin 2019-03-08 17:18:31 +00:00
Laurent Cozic
d9538ccb08 No longer using fixed IDs 2019-03-08 17:17:37 +00:00
Laurent Cozic
9289dbdf77 All: Refactor Markdown rendering (#1315)
* Refactoring MdToHtml to avoid manually rendering tokens

* Minor fix

* Fixed loading of resources

* Handle clicking on checkboxes

* Added back Katex support

* Fixed issues with Katex and note rendering

* Added back support for links

* Restored code block highlighting support

* clean up

* Applying update to mobile

* Fixed handling of links and cleaned up to share more code between mobile and desktop

* Restored content caching and improved handling of additional assets

* Clean up and a few fixes

* Applied more updates to mobile and added code highlighting support
2019-03-08 17:14:17 +00:00
Laurent Cozic
5719ae495a Merge branch 'master' of github.com:laurent22/joplin 2019-03-08 17:13:07 +00:00
Helmut K. C. Tessarek
a89e3b7924 fix another minor typo 2019-03-08 03:34:48 -05:00
Helmut K. C. Tessarek
e576d09712 Merge pull request #1310 from lyndonbarrie/master
fix spelling mistake
2019-03-08 03:12:33 -05:00
lyndonbarrie
43600a7824 fix spelling mistake 2019-03-07 09:13:34 +00:00
Laurent Cozic
921f01d9dc Desktop: Fixed regression on external edit 2019-03-06 22:22:37 +00:00
Laurent Cozic
dec5668582 CLI: Added command to decrypt a file 2019-03-05 20:57:15 +00:00
Laurent Cozic
e30bc12354 All: Disabled Mermaid support for now 2019-03-06 07:11:55 +00:00
Laurent Cozic
fca4fa666d Electron release v1.0.138 2019-03-03 17:14:28 +00:00
Laurent Cozic
59478160c8 Added Arabic translation 2019-03-03 17:14:17 +00:00
Laurent Cozic
8110fe89ef Electron release v1.0.137 2019-03-03 00:59:04 +00:00
Laurent Cozic
c7ed1b5eae Trying to fix Windows 32-bit build 2019-03-03 00:58:41 +00:00
Laurent Cozic
25951e7097 Trying to fix Windows 32-bit build 2019-03-03 00:51:54 +00:00
Laurent Cozic
687b9d1bef All: Improve rendering of Mermaid graphs 2019-03-03 00:31:41 +00:00
Laurent Cozic
630e77b9eb Electron release v1.0.136 2019-03-02 23:40:58 +00:00
Laurent Cozic
68ff2e17b3 Trying to fix Windows 32-bit build 2019-03-02 23:35:18 +00:00
Laurent Cozic
945d83608a All: Include injected Mermaid JS only when necessary 2019-03-02 23:22:41 +00:00
Laurent Cozic
833d473268 Desktop: Resolves #206: Added support for sorting notebooks by title or last modified 2019-03-02 17:35:57 +00:00
Laurent Cozic
2256b0c5ec Desktop: Fixes very slow startup since Electron 4 upgrade 2019-02-28 23:24:28 +00:00
Laurent Cozic
677aa7d59b Merge branch 'master' of github.com:laurent22/joplin 2019-02-28 22:32:47 +00:00
Laurent Cozic
4363005e92 All: Resolves #1105: Added support for macro persistence for Katex 2019-02-28 22:32:29 +00:00
Helmut K. C. Tessarek
2e3ef618db Merge pull request #1271 from RenatoXSR/pt_BR-update-20190227
Updated pt_BR locale to correct unicode char escapes.
2019-02-27 21:16:43 -05:00
Laurent Cozic
1adbbd14c6 Fix images 2019-02-28 00:36:44 +00:00
Laurent Cozic
4dfd7db729 Fixing image 2019-02-28 00:28:46 +00:00
Laurent Cozic
e70562a102 Fixing image 2019-02-28 00:27:25 +00:00
Laurent Cozic
a0e5947ba4 All: Resolves #163: Added Mermaid support to desktop and mobile 2019-02-28 00:22:53 +00:00
Laurent Cozic
e841ea8a91 Fixing bad commit 2019-02-27 23:41:32 +00:00
Laurent Cozic
770a435029 Revert "Revert "Update website""
This reverts commit 49b56e84a7.
2019-02-27 23:38:50 +00:00
Laurent Cozic
49b56e84a7 Revert "Update website"
This reverts commit ff1a6fdbbd.
2019-02-27 23:38:21 +00:00
Laurent Cozic
ff1a6fdbbd Update website 2019-02-27 23:37:16 +00:00
Laurent Cozic
2168090b96 Electron release v1.0.135 2019-02-27 22:52:08 +00:00
Laurent Cozic
33f7b680bc Desktop: Fixes #1273: Trying to improve accuracy of sidebar resizing 2019-02-27 22:44:10 +00:00
Laurent Cozic
0957298cb8 Desktop: Fixes #1268: Fix exporting to PDF and printing, which were broken following upgrade to Electron 4 2019-02-27 22:35:26 +00:00
Laurent Cozic
08f2f982cf Update website 2019-02-27 15:04:34 +00:00
RenatoXSR
3376fbfa55 Updated pt_BR locale to correct unicode char escapes. 2019-02-27 10:31:54 -03:00
Laurent Cozic
4a31e5fe73 Electron release v1.0.134 2019-02-27 10:03:07 +00:00
Laurent Cozic
baacec5ba6 Electron release v1.0.133 2019-02-27 00:14:24 +00:00
Laurent Cozic
95188b71b8 Desktop: Fixes #1265: Fixed column resize on Windows. Also increased minimum and maximum sizes of columns. 2019-02-27 00:13:56 +00:00
Laurent Cozic
cf57be6e98 Merge branch 'master' of github.com:laurent22/joplin 2019-02-26 23:40:16 +00:00
Laurent Cozic
b691092d7a iOS v10.0.30 2019-02-26 23:39:52 +00:00
Helmut K. C. Tessarek
03e60fc028 Merge pull request #1262 from cybertramp/patch-1
한국말 -> 한국어
2019-02-26 16:52:16 -05:00
Andros Fenollosa
2e25ec318f Update language es_ES (#1257)
* Update language es_ES

* Add Last-Translator

* Translation fix
2019-02-26 16:51:51 -05:00
cybertramp
7236e5e9ae 한국말 -> 한국어
"한국말" -> "한국어"
Korean translation error update.
2019-02-26 19:36:45 +09:00
Helmut K. C. Tessarek
6f7dd51a98 Merge pull request #1261 from tessus/localization-de_DE
Update de_DE.po
2019-02-25 20:32:09 -05:00
Helmut K. C. Tessarek
db1dab9293 Update de_DE.po 2019-02-25 20:24:59 -05:00
Laurent Cozic
06f1b9e4d7 Electron release v1.0.132 2019-02-24 23:46:17 +00:00
Laurent Cozic
8f958ac931 Desktop: Trying to fix sign server 2019-02-24 23:46:05 +00:00
Laurent Cozic
eae63bfb79 Electron release v1.0.131 2019-02-24 19:30:09 +00:00
Laurent Cozic
8adfc81c30 Trying to fix Windows build 2019-02-24 19:30:04 +00:00
Laurent Cozic
0c516443e3 Android release v1.0.239 2019-02-24 18:08:54 +00:00
Laurent Cozic
ad9bc0bf63 Electron release v1.0.130 2019-02-24 18:06:09 +00:00
Laurent Cozic
b0596670a6 Revert "New tag adding dialogue (#1206)"
This reverts commit 229dd7a6dd.
2019-02-24 18:04:25 +00:00
Laurent Cozic
998011ff43 Mobile: Apply tag duplication fix to mobile too 2019-02-24 18:02:50 +00:00
Laurent Cozic
081e1c5b62 Electron release v1.0.129 2019-02-24 17:27:11 +00:00
Laurent Cozic
edfd2c4d54 Desktop: Updated build process to fix buggy electron-builder https://github.com/electron-userland/electron-builder/issues/3660#issuecomment-465461491 2019-02-24 17:26:46 +00:00
Laurent Cozic
9d65a3a34c Android release v1.0.238 2019-02-24 12:53:00 +00:00
Laurent Cozic
1a86cbdb9d Electron release v1.0.128 2019-02-24 12:48:14 +00:00
Laurent Cozic
849cb4456c Update website 2019-02-24 12:38:20 +00:00
Laurent Cozic
1736717f2e Api: Require token for search end point, fixed a few issues and added doc 2019-02-24 12:37:37 +00:00
Laurent Cozic
50b75e1e63 API: Resolves #1148: Added support for search end-point and improved error handling 2019-02-24 12:24:55 +00:00
Laurent Cozic
179005dd6c All: Resolves #1198: In search results, give more weight to more recent notes and less to completed to-dos 2019-02-24 12:00:06 +00:00
Laurent Cozic
6b3fe6b2cb Update translations 2019-02-24 11:27:47 +00:00
Helmut K. C. Tessarek
c34872bb26 when deleting notebook or tag, display name (#1253)
The delete dialog for notebooks and tags does not display the name of the item to be deleted.
This can lead to an undesired result.

closes #1245
2019-02-24 11:11:34 +00:00
Helmut K. C. Tessarek
4845a21287 change notebook icon in toolbar and search result (#1252)
see f6f0bcf1c3
2019-02-24 11:10:55 +00:00
Laurent Cozic
ddd513fe09 Small tweaks to avoid confusion with enum name 2019-02-24 11:10:22 +00:00
Helmut K. C. Tessarek
4ce118d459 add shortcut to start a sync (#1231)
see https://github.com/laurent22/joplin/issues/1151#issuecomment-463845158
2019-02-24 11:09:15 +00:00
ruuti
99da184ba5 Set theme based background color to note webview to avoid white background on scroll when dark theme is enabled. (#1230) 2019-02-24 11:07:49 +00:00
Polshakov Dmitry
e2e4e62c4f Desktop: Fix alt key using (#1216)
Fix #934

On linux if you try to change the layout with Alt + Shift, the menu
will open instead
2019-02-24 11:07:07 +00:00
Caleb John
229dd7a6dd New tag adding dialogue (#1206)
* Update tag adding dialogue
- use datalist to autocomplete tags
- display tags with TagList

* Move the tagItem highlight color to theme.js
2019-02-24 10:55:19 +00:00
Mats Estensen
1e0c4cc5cd Desktop: Add type 'info' and 'OK' button to update dialog when version up-to-date (#1148)
* Add type 'info' and 'OK' button to update dialogue when current version is up to date

* remove whitespace and fix space to tabs

* remove created newline at end of file
2019-02-24 10:54:50 +00:00
Laurent Cozic
b40ccc7a15 All: Fixes #1251: Handle Show Uncompleted Tasks option when selecting a tag 2019-02-24 10:38:06 +00:00
Laurent Cozic
7d6b7e588c Desktop: use nodeIntegration 2019-02-24 10:17:37 +00:00
Laurent Cozic
22cacd2c5b Update website 2019-02-23 15:54:49 +00:00
Laurent Cozic
6a22e7836a Website: Add prerelease page 2019-02-23 15:54:34 +00:00
Laurent Cozic
32a67b9b33 minor tweaks 2019-02-23 15:53:14 +00:00
Laurent Cozic
b5dff09c28 All: Simplify Welcome notes and folders to avoid sync problems. Duplicate items will have to be manually deleted. 2019-02-23 15:47:29 +00:00
Laurent Cozic
c56d8153e8 Merge branch 'master' of github.com:laurent22/joplin 2019-02-18 18:12:00 +00:00
Laurent Cozic
eb5950d126 Desktop: Adding support for Mermaid 2019-02-18 18:11:53 +00:00
Helmut K. C. Tessarek
4241436e40 Merge pull request #1235 from tessus/localization-de_DE
Update de_DE.po
2019-02-18 12:04:47 -05:00
Helmut K. C. Tessarek
e93af7aed5 Update de_DE.po 2019-02-18 11:55:41 -05:00
Helmut K. C. Tessarek
d2416f850e Merge pull request #1234 from sczhg/master
Fix unclear Chinese translation.
2019-02-18 04:25:27 -05:00
Hao Huang
7af22eb006 Fix unclear Chinese translation. 2019-02-18 17:13:51 +08:00
Laurent Cozic
3f1be5e7e7 Merge branch 'master' of github.com:laurent22/joplin 2019-02-18 00:43:21 +00:00
Laurent Cozic
a4e649c82d Desktop: Clean up adding custom CSS and remove top margin and padding from rendered html 2019-02-18 00:42:52 +00:00
Laurent Cozic
cde1a8f0a8 Clipper: Fixed build error following package upgrade 2019-02-18 00:40:26 +00:00
Caleb John
cda6eb7c2f Fix little white corner between scrollbars (#1229) 2019-02-17 02:16:32 +00:00
Laurent Cozic
3c3e6aeca0 Merge branch 'master' of github.com:laurent22/joplin 2019-02-16 13:13:39 +00:00
Andros Fenollosa
99156311db Less verbose and visually more informative (#1226) 2019-02-16 13:13:33 +00:00
Laurent Cozic
91b2e5e703 Added Russian translation 2019-02-16 13:12:46 +00:00
Andros Fenollosa
573fd816d0 Update es_ES.po (#1227) 2019-02-16 13:10:54 +00:00
Laurent Cozic
e6aa002758 Desktop: Fixes #1215: Updated single instance logic for Electron 4 2019-02-16 13:10:37 +00:00
Laurent Cozic
361d46ac5d Clean up 2019-02-16 01:17:43 +00:00
Laurent Cozic
9bc7c2fd65 Added comments 2019-02-16 01:15:30 +00:00
Laurent Cozic
ce49f5f8b7 Merge branch 'master' of github.com:laurent22/joplin 2019-02-16 01:12:53 +00:00
Laurent Cozic
81e4cd319d Desktop: Allow resizing left panels 2019-02-16 01:12:43 +00:00
Zorbey Doğangüneş
71f905535f Turkish Language Improving (#1218)
* Fix & Improve

* Update tr_TR.po

Added missing " #, fuzzy " lanes.

* Update tr_TR.po

letter correction

* Update tr_TR.po

removed duplicate lanes.

* Update tr_TR.po

unnecessary space removed
2019-02-15 19:11:04 +00:00
0ndrey
d3bff0a9e3 Update ru_RU.po (#1219) 2019-02-15 19:10:37 +00:00
Laurent Cozic
88e6315d09 Security: Updated a few packages to fix CVE-2018-16469 2019-02-15 18:01:04 +00:00
Laurent Cozic
3d933c5244 Desktop: Trying to upgrade to Electron 4.0.4 2019-02-15 00:30:03 +00:00
Laurent Cozic
73af19314d Desktop: Trying to upgrade to Electron 4.0.4 2019-02-15 00:29:48 +00:00
Laurent Cozic
1d71712c8a Security: Updated parse-url to fix CVE-2018-3774 2019-02-14 23:18:17 +00:00
Laurent Cozic
1333c35389 Update website 2019-02-14 23:13:32 +00:00
ruuti
e0f5f47a15 Load background color from theme colours and set it for top and bottom safe area to respect iOS style guide. (#1195) 2019-02-14 23:09:21 +00:00
N Jannasch
34323042d5 Clipper: Feature added for storing urls (#1173)
* Clipper: Feature added for storing urls

* Clipper: Clip URL variable nameing changed
2019-02-14 23:05:28 +00:00
Joybin Chen
aa86fa9986 Clipper: bugfix, joplin icon not show up in Firefox using 32px size. (#1152) 2019-02-14 23:03:01 +00:00
Laurent Cozic
d2d659d5a9 Android release v1.0.237 2019-02-14 22:32:17 +00:00
Laurent Cozic
1595248b52 Electron release v1.0.127 2019-02-14 22:29:19 +00:00
Laurent Cozic
fc94c616b5 All: Updated translations 2019-02-14 22:28:04 +00:00
Laurent Cozic
f6f0bcf1c3 Desktop: Changed Folder icon to a Notebook 2019-02-14 22:27:53 +00:00
Laurent Cozic
6eeeda5dab All: Update welcome assets 2019-02-14 22:22:43 +00:00
Caleb John
58993d2ead Add styling to the scrollbars (#1204) 2019-02-14 22:21:21 +00:00
Caleb John
7c0b608769 Minor update to dark theme (#1181) 2019-02-14 22:18:42 +00:00
Zorbey Doğangüneş
259be84a3e Turkish Language Support (#1211)
* Add files via upload

* Delete tr_TR.po

* Updated translations
2019-02-14 22:17:36 +00:00
0ndrey
57c880cf85 Russian translate (#1179)
* Исправлен и обновлен перевод

* Update ru_RU.po

* Update ru_RU.po
2019-02-14 22:15:33 +00:00
Laurent Cozic
0469fe76d7 Desktop: Do not display tags that are not associated with any note 2019-02-13 23:33:07 +00:00
Laurent Cozic
a3e74320fa Desktop: Allow creating new note when multiple notes are selected 2019-02-13 22:57:43 +00:00
Laurent Cozic
4e0f4397b2 All: Fixed percent translated and added missing strings 2019-02-13 22:52:32 +00:00
Laurent Cozic
b26aab3863 Desktop: Print background images and colors, and improved checkoxes to avoid use of svg 2019-02-13 22:35:57 +00:00
Laurent Cozic
75ec97fe61 Updated Swedish translation 2019-02-13 17:34:26 +00:00
Laurent Cozic
9a356453fc Fix readme 2019-02-12 17:51:13 +00:00
Laurent Cozic
f0020b3393 Desktop: Highlight row backgroung in table 2019-02-10 17:16:11 +00:00
Laurent Cozic
ea9f1dc91d CLI v1.0.123 2019-02-10 16:47:42 +00:00
Laurent Cozic
2ef77dcf1f Revert "CLI: improve e2ee decrypt command"
This reverts commit 6e235605ed.
Fixed #1144: e2ee decrypt cannot be used inside cron. If we need
manually decrypting a file, we'll need to move this command
somewhere else.
2019-02-10 16:44:07 +00:00
Laurent Cozic
29e7ec4cc9 CLI: Fixes #1194: Fixed opening then saving note in text editor 2019-02-10 16:41:14 +00:00
Laurent Cozic
5710e3fad0 CLI v1.0.122 2019-02-10 16:34:41 +00:00
Laurent Cozic
a03aa62d58 Desktop: Moved buttons up on Config screen for consistency with other screens 2019-02-10 16:29:06 +00:00
Laurent Cozic
2203a39917 Desktop: Disable userstyle.css when printing or exporting to PDF 2019-02-10 16:11:41 +00:00
Laurent Cozic
bc58668483 Desktop: Fixes #1200: Note list was hidden when minimizing and
maximizing window
2019-02-10 15:59:28 +00:00
Ladislav Benc
0b4650f355 Suggesting some language improvements for the Welcome page (#1199)
I'm not sure about the "Joplin has three main columns" part. While it would be more correct to put definite articles
2019-02-10 15:44:43 +00:00
Laurent Cozic
aecdec48ad Electron release v1.0.126 2019-02-09 19:32:04 +00:00
Laurent Cozic
e49198a0d4 oops 2019-02-09 19:31:07 +00:00
Laurent Cozic
6aa4553dd3 Updated translations 2019-02-09 19:28:19 +00:00
Laurent Cozic
860e8a8f5a Desktop: Added a few more shortcuts for macOS and other platforms 2019-02-09 19:13:23 +00:00
Laurent Cozic
434037d793 Done Welcome integration on mobile and CLI 2019-02-09 19:04:34 +00:00
Laurent Cozic
214eae27da CLI: Fixed performance issue which was causing CLI to wait up to 10 seconds when closing the app 2019-02-09 19:04:00 +00:00
Laurent Cozic
0567188fa8 Desktop: Select Welcome notebook the first time it is created 2019-02-08 23:38:29 +00:00
Laurent Cozic
4326902683 Desktop: Fixes #1196: Moved webview to separate component to more precisely handle lifecycle events and precent uncessary reloading 2019-02-08 23:07:01 +00:00
Laurent Cozic
da3589149d Desktop: Fixed restored scroll position when only viewer is visible 2019-02-08 08:28:27 +00:00
Laurent Cozic
69b4b4d1f4 Deskop: Integrated Welcome notes to desktop 2019-02-07 23:27:50 +00:00
Laurent Cozic
dd4b46a88b All: Finished Welcome notebook 2019-02-07 23:17:58 +00:00
Laurent Cozic
6f2253b2f4 Updated Chinese translation (Thanks HongSen) 2019-02-07 22:25:50 +00:00
Laurent Cozic
4c00d9512e Desktop: Resolves #1041: Go back to previous note after clicking link 2019-02-07 18:17:09 +00:00
Laurent Cozic
6f511cb1e6 All: Tweaked blockquote CSS to make it fit more with styling update 2019-02-07 08:23:44 +00:00
Caleb John
029e84f538 Add new styling to tables and blockquote (#1185)
* Add new styling to tables and blockquote

* Update styling to tables
2019-02-07 08:01:38 +00:00
Laurent Cozic
42b1db1d08 Desktop: Use Arial for note viewer in Windows 2019-02-07 00:00:08 +00:00
Laurent Cozic
9d4b34cad7 All: Applied styles to mobile too 2019-02-06 23:52:29 +00:00
Laurent Cozic
cd9aff0f59 Desktop: Make default editor font a bit bigger 2019-02-06 23:06:37 +00:00
Laurent Cozic
032816fffc All: Made note font slightly smaller 2019-02-06 22:48:02 +00:00
Laurent Cozic
1408f06c8d All: darker text 2019-02-06 22:39:03 +00:00
Laurent Cozic
49f8d0c6d8 API: Allow specifying item ID for any item 2019-02-06 22:36:39 +00:00
Laurent Cozic
0d6443c30a All: Improve styling and layout of notes in note viewer 2019-02-05 23:55:09 +00:00
Laurent Cozic
e62d91dda8 Desktop: Allow using macOS App bundle as external editor, and improved error handling 2019-02-05 22:00:25 +00:00
Laurent Cozic
0e122c9dc5 Merge branch 'welcome' 2019-02-05 21:14:43 +00:00
Laurent Cozic
c2bd453e8c Updated welcome notes 2019-02-05 21:14:32 +00:00
Laurent Cozic
949ea7afb7 Merge branch 'welcome' of github.com:laurent22/joplin into welcome 2019-02-05 18:46:41 +00:00
Laurent Cozic
9f575101d2 Updated doc 2019-02-05 18:46:32 +00:00
Laurent Cozic
2b4470054e Merge branch 'welcome' of github.com:laurent22/joplin into welcome 2019-02-05 18:45:24 +00:00
Laurent Cozic
b220613e54 Remove logo 2019-02-05 18:45:16 +00:00
Laurent Cozic
11328babe8 Add NC logo 2019-02-05 18:26:49 +00:00
Laurent Cozic
735bc92bc4 Mobile: Added shim.createResourceFromPath to support Welcome assets 2019-02-05 18:11:03 +00:00
Laurent Cozic
6894b9b1b7 All: Added tests for Welcome asset handling 2019-02-05 17:39:10 +00:00
Laurent Cozic
4de7815f31 doc 2019-02-05 17:08:25 +00:00
Laurent Cozic
4ce7b48468 Merge branch 'master' of github.com:laurent22/joplin 2019-02-05 17:07:41 +00:00
Laurent Cozic
77e4cb87ad iOS v29 2019-02-05 17:06:36 +00:00
Laurent Cozic
dd6b43035e Link between notes on Welcome notes 2019-02-03 20:30:59 +00:00
Laurent Cozic
e76094c546 Also set tags on Welcome notes 2019-02-03 20:19:17 +00:00
Laurent Cozic
9c00dc4cab All: Added script to build welcome assets and to import them on startup 2019-02-03 18:58:44 +00:00
Laurent Cozic
fc416de348 Doc: Removed note 'tags' property from doc as not fully supported (only used in Web Clipper) 2019-02-03 17:01:49 +00:00
Laurent Cozic
a2156be4ec Mobile: Fixed search screen 2019-02-03 16:57:29 +00:00
Laurent Cozic
cf427eba0f Mobile: Fixed sidebar error 2019-02-03 16:15:07 +00:00
Laurent Cozic
0050c90678 Update translations 2019-02-03 15:57:10 +00:00
Laurent Cozic
5eeff02dbe Revert "Исправлен и обновлен перевод (#1175)"
This reverts commit eb283efc20.
2019-02-03 15:56:20 +00:00
0ndrey
eb283efc20 Исправлен и обновлен перевод (#1175) 2019-02-03 15:50:30 +00:00
Laurent Cozic
87121c9c21 All: Fixed importing ENEX file when note incorrectly contains a reminder tag 2019-02-02 13:04:42 +00:00
Laurent Cozic
a2dbbbf832 Merge branch 'master' of github.com:laurent22/joplin 2019-02-02 10:54:07 +00:00
Laurent Cozic
fd251cd9a9 Desktop: started writing welcome notes 2019-02-02 10:53:57 +00:00
Laurent Cozic
8ced2d288e Desktop: Note reloading issue 2019-02-02 10:47:26 +00:00
Laurent Cozic
242926d381 Fixed regressions in test units 2019-01-31 09:48:48 +00:00
Laurent Cozic
8c9a148e71 All: Better handle search queries that include dashes 2019-01-31 08:35:41 +00:00
Laurent Cozic
9e165fc7dc Desktop: Fixes #1161: Display highlighted text and other background colours and images when exporting to PDF or printing 2019-01-31 08:23:47 +00:00
Laurent Cozic
f46e4e0cec Desktop: Fixes #1142: Disallow dropping notes on sidebar Notebook header 2019-01-31 08:02:12 +00:00
Laurent Cozic
efcf5ecef4 Desktop: When deleting note, display title or number of notes 2019-01-31 07:55:51 +00:00
Laurent Cozic
b6ba843d09 Desktop: Added 'Insert date time' option to menu 2019-01-30 22:45:49 +00:00
Laurent Cozic
915112e274 Desktop: Restore scroll position when switching notes 2019-01-30 22:45:28 +00:00
Laurent Cozic
cc8f8fcd2c Desktop: Allow selecting editor path with dialog window 2019-01-30 19:19:06 +00:00
Laurent Cozic
bda3ea9a35 Desktop: Improve how new notes are created to make it more reliable 2019-01-30 18:06:47 +00:00
Laurent Cozic
a7aed1f93a Desktop: Fixed scrolling issue 2019-01-30 17:35:41 +00:00
Laurent Cozic
a33f602f3b Desktop: Record last selected note IDs and restore it when opening notebook 2019-01-29 18:32:52 +00:00
Laurent Cozic
4d08b49578 Desktop: Added Usage link next to search box 2019-01-29 18:11:31 +00:00
Laurent Cozic
21e049ab45 Desktop: When selecting multiple notes, display possible actions as buttons 2019-01-29 18:02:34 +00:00
Helmut K. C. Tessarek
1d4234caea Merge pull request #1155 from chenlhlinux/patch-1
Update zh_CN.po
2019-01-29 01:10:48 -05:00
Laurent Cozic
d1269de3a7 Desktop: Made confirmation buttons sticky on Config screen 2019-01-27 23:44:16 +00:00
Laurent Cozic
8c19fcf8fc Merge branch 'master' of github.com:laurent22/joplin 2019-01-27 23:16:37 +00:00
Laurent Cozic
beaba2be55 Desktop: Group config options into sections 2019-01-27 23:15:56 +00:00
chenlhlinux
32c9ad1d59 Update zh_CN.po 2019-01-27 19:35:10 +08:00
Helmut K. C. Tessarek
a194513252 de_DE.po: remove the last 2 occurences of fuzzy
Both 2 translations are accurate, thus I'm removing the fuzzy flag.
2019-01-26 22:22:09 -05:00
Helmut K. C. Tessarek
cd93a1d1e1 Merge pull request #1157 from xissy/patch-2
All: Update Korean translations
2019-01-26 21:53:36 -05:00
Taeho Kim
2867728996 Update ko.po
Added proper Korean translations into empty messages and fuzzy translations.
2019-01-26 17:34:57 -08:00
Laurent Cozic
394cc78851 Desktop: Use ENTER and ESCAPE to go to next search result or to close search bar 2019-01-26 18:40:43 +00:00
Laurent Cozic
76f0a26322 Desktop: Keep synchronise button and status at bottom of side bar 2019-01-26 18:36:20 +00:00
Laurent Cozic
92d7a577a0 Desktop: Use SPACE to collapse or expand a folder in sidebar 2019-01-26 18:09:09 +00:00
Laurent Cozic
9c1219b188 Desktop: Use arrow to move between items, and added shortcuts to focus different elements 2019-01-26 18:04:32 +00:00
Laurent Cozic
f62bbfe286 Desktop: Toggle todo checkbox using SPACE key 2019-01-26 15:33:45 +00:00
Laurent Cozic
fef176eb96 Merge branch 'master' of github.com:laurent22/joplin 2019-01-26 15:15:35 +00:00
Laurent Cozic
ed541dac3b Desktop: Delete note using keyboard 2019-01-26 15:15:16 +00:00
chenlhlinux
4a175b2158 Update zh_CN.po 2019-01-26 20:06:08 +08:00
Helmut K. C. Tessarek
4076899e11 Merge pull request #1149 from matsest/update-norwegian-190125
All: Update norwegian translations
2019-01-26 02:43:15 -05:00
Laurent Cozic
998bdf3b56 Desktop: Move up and down in note list using arrow keys 2019-01-25 19:59:36 +00:00
Laurent Cozic
76b211eb6d Electron release v1.0.125 2019-01-25 18:03:43 +00:00
Laurent Cozic
f781cb3922 Android release v1.0.236 2019-01-25 18:00:24 +00:00
Laurent Cozic
ced3e5d623 Android: Trying to remove Firebase libraries 2019-01-25 17:54:43 +00:00
Mats Estensen
2a4812cb87 update norwegian translations 2019-01-25 18:35:08 +01:00
Laurent Cozic
1f384c7ae4 Merge branch 'master' of github.com:laurent22/joplin 2019-01-25 17:14:26 +00:00
Laurent Cozic
01a3285636 Updated doc 2019-01-25 17:14:17 +00:00
Laurent Cozic
53166cb3f5 All: Fixes #1137: Fixed regression on SeaFile sync 2019-01-21 17:34:41 +00:00
Laurent Cozic
893462ae87 Check that resource could be loaded 2019-01-20 16:28:10 +00:00
Laurent Cozic
949dbf45f1 Desktop: Resolves #1099: Show Markdown images in external editor 2019-01-20 16:27:33 +00:00
Laurent Cozic
d7dc625042 Clipper: Fixes #1058: Import images at correct size 2019-01-20 15:26:43 +00:00
Laurent Cozic
cc91c77f9e Mobile: Prevent search box from being cleared when updated from somewhere else 2019-01-19 18:40:21 +00:00
Laurent Cozic
4847fd76de Android release v1.0.235 2019-01-19 18:26:17 +00:00
Laurent Cozic
25b711a8da Electron release v1.0.124 2019-01-19 18:23:00 +00:00
Laurent Cozic
b5e50fa62e Update translations 2019-01-19 18:22:18 +00:00
Laurent Cozic
28e40a5c86 Disable a few more tags when renderering to HTML 2019-01-19 18:14:01 +00:00
Laurent Cozic
a8a7b7c07b Merge branch 'master' into search_engine_fts_fix 2019-01-19 18:08:45 +00:00
Laurent Cozic
299008688d All: Search: Integration to CLI and mobile apps 2019-01-19 18:03:05 +00:00
Laurent Cozic
42a674008f All: Search: More multi-language support, and started updating mobile app 2019-01-18 18:31:07 +00:00
Laurent Cozic
8fdc0bf17c All: Search: More multi-language support 2019-01-18 17:56:56 +00:00
Laurent Cozic
4e3896c108 iOS: Fixes #792: Fix "Network request failed" error that some users were having 2019-01-18 17:42:46 +00:00
Laurent Cozic
96cd56548e Desktop: Improve search keyword highlighting 2019-01-17 19:01:35 +00:00
Joybin Chen
739fb2c3d2 Desktop: handle ESC key press to cancel the NotePropertiesDialog (#1125)
* Electron: enable ESC key press to dismiss NotePropertyDialog, when no editedKey.

* Desktop: handle ESC key press to cancel the NotePropertiesDialog
2019-01-16 20:19:48 +00:00
Laurent Cozic
0c98573700 All: Fixes #769: Disable META tags in Markdown viewers 2019-01-15 20:12:53 +00:00
Laurent Cozic
8dc0b34fdc Desktop: Search engine: Improved support for JA, KO, ZH scripts 2019-01-15 19:55:58 +00:00
Laurent Cozic
384ca09842 Merge branch 'master' into search_engine_fts_fix 2019-01-15 19:33:42 +00:00
Laurent Cozic
97d86825c2 Desktop: Fixes #1126: Open Katex link in external browser instead of inside app 2019-01-15 19:30:45 +00:00
Laurent Cozic
f5a824b1e6 Removing CLI demo 2019-01-15 19:12:19 +00:00
Laurent Cozic
4fc11e77e8 Removing CLI demo 2019-01-15 19:11:35 +00:00
Laurent Cozic
8d16ad7035 Updated dates 2019-01-15 19:10:27 +00:00
Laurent Cozic
3b1d84b00b Merge branch 'master' of github.com:laurent22/joplin 2019-01-15 18:37:54 +00:00
Laurent Cozic
3f540da31b Desktop: Improved local search by highlighting even partial matches 2019-01-15 18:37:44 +00:00
Laurent Cozic
3a20f1c245 Merge pull request #1119 from abonte/update-italian-translation
update Italian
2019-01-15 18:18:48 +00:00
Laurent Cozic
e803f0c545 update new strings to nb_NO (#1121) 2019-01-15 18:18:34 +00:00
Philipp Zumstein
c9495c23a6 Update de_DE.po (#1124) 2019-01-15 18:18:17 +00:00
Laurent Cozic
26aae9eea5 Merge branch 'master' of github.com:laurent22/joplin 2019-01-15 18:17:56 +00:00
Laurent Cozic
7d92136467 Added version info to auto-update dialog 2019-01-15 18:17:45 +00:00
Laurent Cozic
a7896b43d7 More search engine improvements 2019-01-15 18:10:22 +00:00
Laurent Cozic
2e12b2655b More fixes to search engine and better handling of non-ASCII searches 2019-01-14 19:11:54 +00:00
Laurent Cozic
a1f0bd1e6c Search engine: normalize text 2019-01-13 16:05:07 +00:00
Laurent Cozic
4472590133 Merge branch 'master' into search_engine_fts_fix 2019-01-13 15:52:41 +00:00
Laurent Cozic
64f1214ad9 Merge branch 'master' of github.com:laurent22/joplin 2019-01-13 15:50:37 +00:00
Laurent Cozic
bd465a72cf iOS v10.0.28 2019-01-13 15:50:19 +00:00
Mats Estensen
1d1c2a6925 update new strings to nb_NO 2019-01-12 14:40:57 +01:00
abonte
d68ba32533 fix typo 2019-01-12 14:28:49 +01:00
Laurent Cozic
d1a316032d Update website 2019-01-12 00:02:23 +00:00
Laurent Cozic
b465042a56 Fixed stats page 2019-01-12 00:01:58 +00:00
Laurent Cozic
8ff2418b02 Desktop: Added support for pre-releases 2019-01-11 23:40:05 +00:00
Laurent Cozic
f6640bcc32 Electron release v1.0.123 2019-01-11 22:07:23 +00:00
Laurent Cozic
fa3c0fd18a Android release v1.0.234 2019-01-11 17:14:19 +00:00
Laurent Cozic
2ac03c18c4 Revert "replace markdown-it-katex with markdown-it-texmath, this makes using $ much better (#1116)"
This reverts commit 46b82f877b.
2019-01-11 17:02:04 +00:00
abonte
51ee6128f3 update italian 2019-01-11 17:47:17 +01:00
Laurent Cozic
53478056de Update website 2019-01-10 21:43:25 +00:00
Laurent Cozic
83c791564a Android release v1.0.233 2019-01-10 19:14:32 +00:00
Laurent Cozic
65d0032995 Electron release v1.0.120 2019-01-10 19:04:35 +00:00
Laurent Cozic
37c4f99341 CLI v1.0.120 2019-01-10 19:00:42 +00:00
Laurent Cozic
adbc873b2a Update translations 2019-01-10 18:59:56 +00:00
Laurent Cozic
3567a57d6a Update for mac 2019-01-10 18:53:18 +00:00
Caleb John
b4e9fb157f Apply zoom and editorfont updates without needing to restart (#1109)
* Apply zoom and editorfont updates without needing to restart
fixes #1106

* Combine zoomRatio and editorFontSize into the theme cache
2019-01-10 18:34:58 +00:00
Helmut K. C. Tessarek
1be3646a04 fix PR template (#1114)
If someone does not delete the 'Attention' line, it will show up in the pull request.
Comments and information should be enclosed in comment markers.
2019-01-10 18:34:24 +00:00
Caleb John
46b82f877b replace markdown-it-katex with markdown-it-texmath, this makes using $ much better (#1116) 2019-01-10 18:33:49 +00:00
Helmut K. C. Tessarek
ef56eb4a52 fix markdown code for checked checkbox (#1113)
fixes #1067
2019-01-10 18:32:45 +00:00
Laurent Cozic
6989f9fd16 CLI: Fixes #1096: Fixed search function in terminal app 2019-01-10 19:25:21 +00:00
Laurent Cozic
7c3e8547de CLI: Fixes #1100: New folders were no longer being shown in list 2019-01-10 19:17:38 +00:00
Laurent Cozic
8268c3edba Desktop: Resolves #1059: Fixed behaviour of export to PDF and print 2019-01-10 18:58:58 +00:00
Laurent Cozic
a8cc8763b0 Android: Fixes #321: Removed dependency to Firebase 2019-01-10 18:49:26 +00:00
Laurent Cozic
09b4acf087 Merge branch 'master' of github.com:laurent22/joplin 2019-01-10 18:04:20 +00:00
Laurent Cozic
3b719ce53b Fixed keyword highlighting bug and other minor issues 2019-01-09 17:33:52 +00:00
Abijeet Patro
83281197f1 Adds functionality to toggle the notebooks and tags on the sidebar. (#1002)
* Adds functionality to toggle the notebooks and tags on the sidebar.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Modified to not use an anonymous function.

Signed-off-by: abijeetpatro <abijeetpatro@gmail.com>

* Updated the code to be cleaner.

Signed-off-by: abijeet <abijeetpatro@gmail.com>
2019-01-09 17:25:44 +00:00
tfinnberg
ffda04f9b4 fixed file url issue (#1054) 2019-01-09 17:21:32 +00:00
Ikuya AWASHIRO
606893286a Update Japanese Translation. (#1102) 2019-01-09 17:16:49 +00:00
Laurent Cozic
075b71746a Update CONTRIBUTING.md 2019-01-09 17:07:43 +00:00
Laurent Cozic
01f1f3e957 Update PULL_REQUEST_TEMPLATE 2019-01-09 17:06:59 +00:00
Caleb John
88a9d5e802 Fix window manager icon on linux (#1110) 2019-01-09 17:05:28 +00:00
Laurent Cozic
7eebfae1c3 Updated pt_BR translation 2019-01-09 16:49:27 +00:00
Jacob Herrington
340fe76b8f Remove S (#1101)
Noticed this while reading!
2019-01-06 01:54:30 +01:00
Helmut K. C. Tessarek
e83678df3a Merge pull request #1098 from rasperepodvipodvert/patch-1
Update ru_RU translation
2019-01-04 18:07:09 -05:00
Helmut K. C. Tessarek
0bbbb49a31 Merge pull request #1079 from y-usuzumi/master
Update zh_CN translations
2019-01-04 18:06:23 -05:00
Ettore Atalan
0e61115857 Update de_DE.po (#1076)
* Update de_DE.po

Translated last untranslated strings and removed anglicisms.
2019-01-04 18:05:50 -05:00
Mats Estensen
8d3ac6f6fe Update Norwegian translation (#1061)
* Update norwegian translation

* correct plural words and some menu items

* update nb_NO
2019-01-04 18:05:13 -05:00
filatovzru
86e644be9a Добавил перевода 2019-01-04 20:32:59 +03:00
Laurent Cozic
30201249b5 Mobile: Fixes #1068: Handle case where notebook has a parent that no longer exists 2018-12-31 17:33:20 +01:00
Laurent Cozic
41155f5ef4 Started rewriting search engine to better support unicode 2018-12-29 20:19:18 +01:00
Laurent Cozic
f308fe71f9 Mobile: Fixes #1082: Highlight correct keywords when doing a search 2018-12-29 18:24:02 +01:00
Laurent Cozic
5a00214fd2 Android release v1.0.232 2018-12-29 03:14:34 +01:00
Laurent Cozic
1b3e0f65e1 Mobile: Fixes #1057: Handle more cases where the title text field disappear 2018-12-29 03:12:23 +01:00
Laurent Cozic
7cfc537870 Android release v1.0.225 2018-12-28 21:42:58 +01:00
Laurent Cozic
53513db5b5 Mobile: Fixes #1066: Disable use of FTS when not present on device 2018-12-28 21:40:29 +01:00
Laurent Cozic
59402cf198 Android: Fixes #1062: Don t display Play Service alert when Play Service unavailable 2018-12-28 20:38:40 +01:00
Laurent Cozic
12efc02d91 Android release v1.0.224 2018-12-27 22:51:38 +01:00
Laurent Cozic
f38b907680 Mobile: Fixes #1057: Fix missing title field issue in Android 2018-12-27 22:49:19 +01:00
Laurent Cozic
8fcb46ca4a Revert "Revert "Upgrade React Native" - Fixes #1057"
This reverts commit 50ad4d05f2.
2018-12-27 20:14:20 +01:00
Kenneth Zhao
71ec9a193f Update zh_CN translations 2018-12-27 14:27:15 +08:00
Laurent Cozic
393a545548 Android release v1.0.201 2018-12-23 20:28:03 +01:00
Laurent Cozic
f88449fbb0 Android release v1.0.200 2018-12-23 20:18:24 +01:00
Laurent Cozic
50ad4d05f2 Revert "Upgrade React Native" - Fixes #1057
This reverts commit 32c02275a2.
2018-12-23 20:11:12 +01:00
Laurent Cozic
8d0e562c8a Android release v1.0.181 2018-12-20 14:58:35 +01:00
Laurent Cozic
c98e67c003 /bin/bash: qa: command not found 2018-12-20 14:53:10 +01:00
Laurent Cozic
5565538b80 Android: Trying to get notifications to work in Android 8.x 2018-12-20 14:52:56 +01:00
Helmut K. C. Tessarek
958979e1d7 CLI v1.0.119 2018-12-19 11:13:18 +01:00
Laurent Cozic
685845e097 Update website 2018-12-17 23:47:35 +01:00
Laurent Cozic
3813f9e417 Clipper release v1.0.8 2018-12-17 23:42:13 +01:00
Laurent Cozic
40cf3fb4d0 Android release v1.0.179 2018-12-17 23:25:21 +01:00
Laurent Cozic
3f88b16603 iOS v10.0.27 2018-12-17 23:12:35 +01:00
Laurent Cozic
32c02275a2 Upgrade React Native 2018-12-17 23:11:53 +01:00
Laurent Cozic
c0d679b6c2 Android release v1.0.178 2018-12-16 18:41:51 +01:00
Laurent Cozic
eb789b9b9a Electron release v1.0.119 2018-12-16 18:35:37 +01:00
Laurent Cozic
b1898141c3 Mobile: Fixes #382: Implemented new search engine for mobile and highlight searched words in notes 2018-12-16 18:32:42 +01:00
Laurent Cozic
3231bfaff0 Mobile: Fixes #1045: Display notebooks as a tree in notebook dropdown 2018-12-16 17:18:24 +01:00
Laurent Cozic
6bb09c9c30 Updated FAQ with info about F-Droid and how to fix Nextcloud issues 2018-12-16 14:30:56 +01:00
Laurent Cozic
35d3fe03ab Android: Fixes #321: Changed notification library to Firebase to get more reliable notifications 2018-12-16 14:11:45 +01:00
Laurent Cozic
f05929cd17 All: Fixes #1033: Handle hard break when rendering Markdown to HTML 2018-12-16 11:41:15 +01:00
Laurent Cozic
982c9828da Desktop: Fixes #1039: Always print or export to PDF using light theme 2018-12-16 02:49:06 +01:00
Laurent Cozic
d6eacb2b33 Android release v1.0.177 2018-12-15 01:51:49 +01:00
Laurent Cozic
0abe213fc2 Merge branch 'master' of github.com:laurent22/joplin 2018-12-15 01:46:16 +01:00
Laurent Cozic
a6716d55c5 Doc: Added search engine doc 2018-12-15 01:46:06 +01:00
Laurent Cozic
fa0572de77 Mobile: Many small fixes and improvements to style and layout to make app more usable 2018-12-15 01:45:35 +01:00
Laurent Cozic
6dca4a0d6b Mobile: Optimised loading of large notes that contain many images. 2018-12-15 01:42:19 +01:00
Helmut K. C. Tessarek
eacfe1a9ac add Apache WebDAV Module to WebDAV-compatible services (#1042) 2018-12-14 22:50:50 +00:00
Laurent Cozic
c223cdf10a Electron release v1.0.118 2018-12-14 19:57:32 +01:00
Laurent Cozic
38c42b7a15 Merge branch 'full_text_search' 2018-12-14 19:56:21 +01:00
Mats Estensen
56432dc773 [Documentation] Add example editor config to FAQ (#1036)
* add external editor example config to faq

* add external editor to feature list
2018-12-14 18:04:53 +00:00
Lucas
d3b4379161 Update to make it works in Kubuntu (#1038)
The directory ~/.local/share/applications/ exists in Kubuntu 18.04 so with this change the script also work in Kubuntu.
2018-12-14 18:03:39 +00:00
Laurent Cozic
8a6fcdbcae Should be commented out by default 2018-12-14 00:00:03 +01:00
Laurent Cozic
061ce646d2 Finished search engine integration with desktop app 2018-12-13 23:57:14 +01:00
Laurent Cozic
5ec7c16e3e Fixed logic to update search engine data 2018-12-12 22:40:05 +01:00
Andros Fenollosa
5d629508c1 Fix name in Linux Desktop (#1034) 2018-12-11 21:51:47 +01:00
Laurent Cozic
0a6f8b0cfe Started integrating search engine to desktop app 2018-12-10 19:58:49 +01:00
Laurent Cozic
460f826672 Nearly finished search engine backend 2018-12-10 18:54:46 +00:00
Laurent Cozic
cb16a10121 Updated the way item changes are recorded so that info can be used by more services (including search engine) 2018-12-10 01:39:31 +01:00
Laurent Cozic
3b6131f1ca Started support for FTS search 2018-12-09 21:45:50 +01:00
Laurent Cozic
57225a36b9 Updated translations 2018-12-09 01:22:16 +01:00
Laurent Cozic
3e313399c2 Desktop: Search within current note 2018-12-09 01:18:10 +01:00
Laurent Cozic
7947e14792 Merge branch 'master' of github.com:laurent22/joplin 2018-12-08 00:42:54 +01:00
Laurent Cozic
71098102c5 Electron: Fixes #476 (maybe): Trying to fix notification flood. Added more log statements in case something goes wrong. 2018-12-08 00:42:29 +01:00
Laurent Cozic
8e601e80df All: Prevent sync infinite loop under some rare conditions 2018-12-08 00:41:39 +01:00
Caleb John
3b14cfcc54 add separate editor font size option (#1027) 2018-12-07 22:26:03 +01:00
Laurent Cozic
61a0e43092 Mobile: Fixes #999: Associate new note with default notebook when creating it from Welcome screen 2018-12-07 01:23:36 +01:00
Laurent Cozic
d08aaffe41 Mobile: Resolves #1015: (Re-)added support for selecting image from camera roll 2018-12-07 01:07:10 +01:00
Laurent Cozic
7d0def30f0 Revert "ReactNativeClient: A better NAV_BACK logic to change folder or tag. (#984)" (Was opening up side menu on first app startup)
This reverts commit fc8f53fd0e.
2018-12-07 00:45:53 +01:00
Laurent Cozic
bb45d72a56 Clipper: Fixes #1026: Handle adding tag to clipped content after content has been clipped 2018-12-06 23:50:02 +01:00
Laurent Cozic
3943192c5d All: Fixes #808 (maybe): Added fix for Nginx 404 error issue. 2018-12-06 23:09:54 +01:00
Laurent Cozic
18d76807f6 Fixing Norwegian locale 2018-12-06 22:59:08 +01:00
Laurent Cozic
01a30a7ccf Fixed renaming 'no' locale to 'nb_NO' 2018-12-05 23:30:36 +01:00
Mats Estensen
3fb35d043b Revise and complete Norwegian translation (no) and rename to correct locale: nb_NO (#1013)
* update norwegian translation and correct locale name to nb_NO

* set no to nb_NO in documentation

* Add name, email and completion to README

Norwegian translation
2018-12-05 23:21:40 +01:00
Alex Devero
9b51bd484d Fix failing Windows build (#997)
* Upgrade sqlite3 to 4.0.4

* Remove rebuild, add install-app-deps
2018-12-05 23:11:40 +01:00
Laurent Cozic
879b556845 Update website 2018-11-24 12:05:49 +00:00
Laurent Cozic
0df2a501dd Electron release v1.0.117 2018-11-24 11:44:38 +00:00
Laurent Cozic
6f64fdffcc Desktop: Fixes #995: Added flag to disable tag bar for now 2018-11-24 11:42:50 +00:00
Laurent Cozic
19252af345 Desktop: Fixes #996: Allow editing multiple notes in external editor 2018-11-21 19:50:50 +00:00
Laurent Cozic
897f53b13e All: Resolves #846: Set resource path to correct relative path so that for example images show up in Markdown viewers 2018-11-21 00:36:23 +00:00
Laurent Cozic
45cd8b7e3c Merge branch 'master' of github.com:laurent22/joplin 2018-11-20 23:19:07 +00:00
Laurent Cozic
922bbdd1b6 All: Fixes #968: Export resources specified with a title 2018-11-20 23:18:56 +00:00
rhtenhove
c24135577c Useless and error prone VERSION file removed & warn use of root (#989)
* Don't create unused VERSION file

It will throw an error if the script is run from a non-writable directory

* Warn user if running as root

This will write files as root:root

* Clearer root warning
2018-11-20 21:54:58 +00:00
Laurent Cozic
3240ff40bc Restored string to avoid invalidating all the translations. 2018-11-20 21:54:40 +00:00
Ben Fisher
58b68cab0c fix for #906, 1) windows paths like C:\a\b weren't accepted because b… (#935)
* fix for #906, 1) windows paths like C:\a\b weren't accepted because backslashes were treated as escape sequences, 2) common paths like C:\Program Files\Foo\Foo.exe weren't accepted because of the space in the path

* Using anothing approach,
a) backslashes are no longer treated as escape characters,
b) string change to remind people to add spaces

* Removing joplin.pot from the patch, it will be updated later.

* Removing unused code.
2018-11-20 21:46:18 +00:00
Helmut K. C. Tessarek
0a0afd7245 CLI v1.0.118 2018-11-20 15:38:32 -05:00
Laurent Cozic
de01606bff Update website 2018-11-20 19:09:47 +00:00
Laurent Cozic
046474b484 Android release v1.0.176 2018-11-20 00:49:27 +00:00
Laurent Cozic
277b2b9298 Electron release v1.0.116 2018-11-20 00:44:25 +00:00
Laurent Cozic
0b7296ae95 Update translations 2018-11-20 00:43:35 +00:00
Laurent Cozic
ce87dd55f0 Merge branch 'master' of github.com:laurent22/joplin 2018-11-20 00:42:33 +00:00
Laurent Cozic
07b724d65b All: Fixes #992: Allow non-ASCII chars when exporting MD and handle duplicate filenames 2018-11-20 00:42:21 +00:00
Caleb John
bc1984298f Add dark theme to note properties dialog (#991) 2018-11-19 22:48:10 +00:00
Ein Verne
9ed0bdfed2 Add more Chinese translation (#986) 2018-11-19 22:44:20 +00:00
Caleb John
57628e8986 Add missing syntax file for dark theme (#985) 2018-11-19 22:43:56 +00:00
Joybin Chen
fc8f53fd0e ReactNativeClient: A better NAV_BACK logic to change folder or tag. (#984) 2018-11-19 22:43:21 +00:00
Renato Rosa
efd7cc6a0c update-locale-pt_BR (#981)
Update missing strings and fixed flagged ones.
2018-11-19 22:40:21 +00:00
Abijeet Patro
7bfc3e1256 Fixes #979 (#980)
* Adds functionality to display tags under the open note.

Towards #469

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Ensured tags in the dialog box and under the note appear in the same order.

Few formatting tweaks.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Fixes issues raised during code review.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Refactored code to always display tags in ascending order.

This changes the order of the tags in the dialog box and below the tag title.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Added the new tag height and margin bottom to the bottomRowHeight

Fixes #979

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-11-17 11:21:57 +00:00
FleischKarussel
7f6ca1e527 fixed typo in Aktualsierungsdatum to Aktualisierungsdatum (#974) 2018-11-17 11:17:47 +00:00
Laurent Cozic
71d9b1d441 Electron: Fixes #933: Handle internal links from HTML and from MD 2018-11-16 18:39:11 +00:00
Laurent Cozic
a3d64d0a90 Update website 2018-11-16 16:53:37 +00:00
Laurent Cozic
e7ec2ce6cf Electron release v1.0.115 2018-11-13 23:18:08 +00:00
Laurent Cozic
61dbdd5f7c Update translations 2018-11-13 23:17:56 +00:00
Laurent Cozic
e6888c451d Fixing Katex version number 2018-11-13 22:42:06 +00:00
Laurent Cozic
899219abd2 Merge branch 'master' of github.com:laurent22/joplin 2018-11-13 22:38:32 +00:00
Joybin Chen
7a4c7a13eb Fix image fetching error. For url like http://example.com/favicon.ico?ver=1.0 will be changed to http://example.com/favicon.ico?ver=1.0?ver=1.0 (#970) 2018-11-13 22:37:39 +00:00
Laurent Cozic
e8797f49b9 Mobile: Display number of resources being fetched in side bar 2018-11-13 22:27:58 +00:00
Laurent Cozic
e17f3051f0 Display number of resources being fetched in side bar 2018-11-13 22:25:23 +00:00
Laurent Cozic
06091933e1 All: Moved resource app-specific state to different table 2018-11-13 00:45:08 +00:00
Laurent Cozic
b30c65dd89 Update CONTRIBUTING.md 2018-11-11 20:23:55 +00:00
Ben Fisher
0eb18d206d Patch to implement feature, exporting notes to JSON (#912, issues/912). (#927)
* Patch to implement feature, exporting notes to JSON (#912, issues/912).

* Revising based on feedback

* Directly calling JSON.stringify on the item
2018-11-11 20:17:43 +00:00
Laurent Cozic
3a9948e528 Build doc 2018-11-08 01:21:18 +00:00
Laurent Cozic
2bcddd38b2 Update API doc 2018-11-08 01:17:46 +00:00
Laurent Cozic
5ff8808f69 API: Allow setting the ID of newly created notes. 2018-11-08 01:14:13 +00:00
Laurent Cozic
28b1d8a324 Desktop: Fixes #953 (maybe): Improved the way internal links to notes are loaded to make it more reliable 2018-11-08 00:58:06 +00:00
Laurent Cozic
5c1dd79435 All: Fixes #952: Upgraded Katex lib to fix bug 2018-11-07 23:44:59 +00:00
Laurent Cozic
706d59a6cc Doc 2018-11-07 23:43:48 +00:00
Ben Fisher
251f1bba55 Show note title in pdf export (#890) (#937)
* Show note title in pdf export (#890)

An example of a possible approach, to temporarily change the html in the webview to show the note title. Works, but there may be a more elegant fix.

* Show title in pdf export

Revising based on feedback
Also, a couple changes to tests so that they pass in Windows.
2018-11-07 23:35:14 +00:00
Caleb John
cb1fd85ca4 Add support for custom css across all notes (#925) 2018-11-07 22:52:31 +00:00
Laurent Cozic
11ddc55911 Fixed theme handling for new tag feature 2018-11-07 22:47:34 +00:00
Caleb John
ee106105d8 Joplin desktop Dark Mode (#921)
* Added support for the dark mode on desktop

* Add dark highlighting to the code tags

* Update app/theme.js to be more clear and more easily support additional themes
Update more files to conform to theming
2018-11-07 22:37:13 +00:00
Laurent Cozic
19f5a144e5 Added comment 2018-11-07 22:22:26 +00:00
Abijeet Patro
18717bac79 Adds functionality to display tags under the open note. (#893)
* Adds functionality to display tags under the open note.

Towards #469

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Ensured tags in the dialog box and under the note appear in the same order.

Few formatting tweaks.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Fixes issues raised during code review.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>

* Refactored code to always display tags in ascending order.

This changes the order of the tags in the dialog box and below the tag title.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-11-07 22:16:05 +00:00
Laurent Cozic
28fa83c406 Merge branch 'master' of github.com:laurent22/joplin 2018-11-02 19:56:18 +00:00
Laurent Cozic
258e514a91 Marked password cache as secure, and only display stars if the value is non-empty 2018-11-02 19:22:49 +00:00
Ben Fisher
f92546d6eb Adding some tips for building and debugging Joplin on Windows (#938) 2018-11-02 01:06:48 +00:00
sensor-freak
693456164b Minor fix for translation of 'Location' (#936) 2018-11-02 00:53:05 +00:00
Helmut K. C. Tessarek
7cd3e6b1f7 mobile: add version info (#930) 2018-11-02 00:43:42 +00:00
FoxMaSk
764e63d869 new French Translations (#923) 2018-11-02 00:24:28 +00:00
Laurent Cozic
2c6f47f277 Update README.md
Removed Hacktoberfest news
2018-11-01 17:45:05 +00:00
Laurent Cozic
e41896d6f3 All: Resolves #918: Skip properties that are on sync target but not handled by local client 2018-10-31 00:35:57 +00:00
Laurent Cozic
990591cc80 Minor improvments 2018-10-30 00:17:50 +00:00
Laurent Cozic
7b85c33213 CLI v1.0.117 2018-10-29 23:24:19 +00:00
Laurent Cozic
4b4d0e8b25 Update website 2018-10-24 21:14:38 +01:00
Laurent Cozic
4fb6af3c62 Android release v1.0.175 2018-10-24 19:59:37 +01:00
Laurent Cozic
d7ffe7e294 Electron release v1.0.114 2018-10-24 19:52:07 +01:00
Laurent Cozic
3ff139d445 Merge branch 'master' of github.com:laurent22/joplin 2018-10-24 19:51:16 +01:00
Laurent Cozic
40443e0134 Android v153 2018-10-24 19:50:26 +01:00
Laurent Cozic
1f927c1285 iOS v26 2018-10-24 19:48:09 +01:00
Andros Fenollosa
5e82e62335 Linux script install: implement previous updates (#905)
Avoid an erroneous update and prevent installation icon only on gnome.
2018-10-24 19:17:18 +01:00
Yannis Mitsos
de954827df Support FreeBSD in terminal (#896) 2018-10-24 19:16:15 +01:00
Laurent Cozic
2cb24bf198 Mobile: Fixes #902: Don't change existing note when sharing with mobile app 2018-10-24 19:10:05 +01:00
Laurent Cozic
739a6a4a9c Documentation 2018-10-24 18:47:04 +01:00
Laurent Cozic
dfcf1193dc Electron: Handle internal anchors 2018-10-17 08:01:18 +01:00
Laurent Cozic
c72f92e22f Additional info 2018-10-15 18:40:11 +01:00
Laurent Cozic
f6d01ce7e1 Android release v1.0.174 2018-10-15 00:01:36 +01:00
Laurent Cozic
fed9700587 Merge branch 'master' of github.com:laurent22/joplin 2018-10-15 00:00:13 +01:00
Laurent Cozic
12a3a9a89e android release 2018-10-14 23:59:42 +01:00
Laurent Cozic
590c62c371 Merge branch 'fixing_android_build' 2018-10-14 23:56:42 +01:00
Laurent Cozic
df41f64b3c Revert "trying to fix android build"
This reverts commit 621d0260f4.
2018-10-14 21:47:12 +01:00
Laurent Cozic
1849355245 Android: Tryinc to fix release builkd 2018-10-14 21:42:34 +01:00
Laurent Cozic
fa1b471ea4 Android: Tryinc to fix release builkd 2018-10-14 21:41:29 +01:00
Laurent Cozic
0a67f8c947 Revert "Android: Updated project to build on macOS"
This reverts commit b547f9aa13.
2018-10-14 20:44:05 +01:00
Laurent Cozic
621d0260f4 trying to fix android build 2018-10-14 19:57:30 +01:00
Laurent Cozic
f93fca7c5b Revert "Android: Updated project to build on macOS"
This reverts commit b547f9aa13.
2018-10-13 11:35:21 +01:00
Laurent Cozic
f4d830c2ef Android release v1.0.151 2018-10-13 11:09:03 +01:00
Laurent Cozic
1aa2844efa Android release v1.0.148 2018-10-13 10:39:55 +01:00
Laurent Cozic
f22b2adaad Mobile: Improved camera attachment 2018-10-13 10:32:44 +01:00
Laurent Cozic
b547f9aa13 Android: Updated project to build on macOS 2018-10-13 00:30:41 +01:00
Laurent Cozic
e4166e9da7 Electron: Fixes #312 (maybe): Removed power saving feature, which wasn\'t doing anything and added a possible fix to the UI freezing issue on Linux 2018-10-12 23:44:00 +01:00
Laurent Cozic
1634fdb421 Merge branch 'master' of github.com:laurent22/joplin 2018-10-12 23:26:11 +01:00
Laurent Cozic
7f51035f91 Mobile: Reload note when resource got downloaded. Also fixed Android build script to make it work in macOS. 2018-10-12 23:25:11 +01:00
ebayer
70e71cbc2a Mobile: Fixes #856: Add option to open source url (#872)
* Mobile: Fixes #856: Add option to open source url

* Mobile: Fixes #856: Change menu wording for opening source url
2018-10-12 19:30:00 +01:00
Laurent Cozic
ffd03bf34c Merge branch 'master' of github.com:laurent22/joplin 2018-10-11 17:53:40 +01:00
Christian Baer
f59a3dee78 Fixed some more inconsistencies in german language file. (#855)
* Fixed some typos and inconsistencies in the german language file and added missing translations.

* Fixed some more inconsistencies in german language file.
2018-10-11 17:21:12 +01:00
Timothy Cyrus
3ba3037242 Update README.md (#874) 2018-10-11 17:20:42 +01:00
Helmut K. C. Tessarek
dbb269fef6 add support for webp images (#858)
fixes #848
2018-10-11 17:18:33 +01:00
Laurent Cozic
e209189faa All: Added resource test units 2018-10-11 17:18:24 +01:00
Laurent Cozic
2d7065cde2 Android release v1.0.143 2018-10-10 20:36:42 +01:00
Laurent Cozic
59f5972c93 Electron release v1.0.113 2018-10-10 20:32:45 +01:00
Laurent Cozic
8bac5275c3 All: Fixed fetch logic - mark resource as fetched by default, unless it comes from sync 2018-10-10 18:53:09 +01:00
Helmut K. C. Tessarek
58d748e235 fix permissions for shell scripts (add executable flag) (#866) 2018-10-09 22:58:18 +01:00
Laurent Cozic
e69ac3e62a Android release v1.0.142 2018-10-09 22:05:29 +01:00
Laurent Cozic
7fc8ac4c0f Electron release v1.0.112 2018-10-09 22:02:47 +01:00
Laurent Cozic
069dce69cd Mobile: Added support for ResourceFetcher service 2018-10-09 22:01:50 +01:00
Laurent Cozic
3bdf621026 Api: Document tags parameter 2018-10-08 19:38:27 +01:00
Laurent Cozic
2f62897fb6 All: Improved resource side loading 2018-10-08 19:11:53 +01:00
Laurent Cozic
dbdd602f50 All: Created ResourceFetcher class to handle resource downloads 2018-10-08 07:36:45 +01:00
Laurent Cozic
d66fa87b2b All: Allow excluding certain keys during sync 2018-10-07 20:18:43 +01:00
Laurent Cozic
124a959c8d All: Simplifying serialisation of base items 2018-10-07 20:11:33 +01:00
Laurent Cozic
127dce1cd6 Fixed typo 2018-10-07 19:28:19 +01:00
Laurent Cozic
44986a35a4 Android: Fix crash when attaching certain files 2018-10-07 18:55:49 +01:00
Laurent Cozic
ea516301fd Udpated German translation 2018-10-05 19:53:54 +01:00
Christian Baer
90b684457a Fixed some typos and inconsistencies in the german language file and added missing translations. (#854) 2018-10-05 19:53:13 +01:00
Laurent Cozic
8517e2aa42 Mobile: Fixes #840: Images were not being displayed right after being attached in view mode 2018-10-05 19:49:36 +01:00
Laurent Cozic
b880be8b7c All: Fixes #853: Replace characters to equivalent US-ASCII ones when exporting files 2018-10-05 17:53:55 +01:00
Laurent Cozic
57fd1a7588 Electron: Prevent URLs added via A tag from being opened inside app 2018-10-05 18:21:23 +00:00
Laurent Cozic
5ed458f634 Electron: Fixed potential crash that can happen if editor is not ready 2018-10-05 18:19:47 +00:00
Laurent Cozic
ac12143d00 All: Fixes #671: Make string translatable 2018-10-05 18:17:49 +00:00
Helmut K. C. Tessarek
b6c36d1961 fix file permissions (#851) 2018-10-04 22:32:19 +01:00
FoxMaSk
3c2de70baa add shared folder (#850)
shared 📁 is needed, so the encryption menu will be translated
2018-10-04 22:30:48 +01:00
Laurent Cozic
f6c5620682 Electron: Resolves #751: Allow switching between todo and note when multiple notes are selected 2018-10-04 18:34:30 +01:00
Laurent Cozic
79b6f64bd0 Merge branch 'master' of github.com:laurent22/joplin 2018-10-04 18:01:23 +01:00
Laurent Cozic
ed89f55bff Electron: Fixes #847: Prevent view from scrolling to top when clicking checkbox and editor not visible 2018-10-04 17:56:39 +01:00
Laurent Cozic
8841a92142 Update README.md 2018-10-04 09:10:46 +01:00
Laurent Cozic
0bd19c97eb Merge branch 'master' of github.com:laurent22/joplin 2018-10-04 08:53:11 +01:00
Laurent Cozic
2fd026d107 Mentioned Hacktoberfest 🎃 in Readme 2018-10-04 08:51:48 +01:00
Laurent Cozic
5e7eb37ca7 Merge branch 'master' of github.com:laurent22/joplin 2018-10-04 08:18:19 +01:00
Laurent Cozic
6b10d5d821 Api: Fixes #843: Fixed regression that was preventing resource metadata from being downloaded 2018-10-04 08:17:53 +01:00
Laurent Cozic
0f4dbfbcbf Merge pull request #841 from shorty2380/patch-1
Joplin_install_and_update.sh
2018-10-04 08:10:51 +01:00
Laurent Cozic
99493174ec API: Fixed handling of PUT method and log errors to file 2018-10-04 08:05:22 +01:00
Patrick Petermann
333253fd4f Joplin_install_and_update.sh
The script was original written for “Ubuntu – Gnome” only. I change it a little bit to support more distributions and desktop enviroments.

This script could be used to install and update Joplin at several Linux distributions. I could test this script with “Fedora 28 – Cinnamon” and “Mint LMDE 3”.   There are a lot of requests how to install / start Joplin at Linux in the FAQ’s. Hopefully this could help the people
2018-10-03 20:48:14 +02:00
Laurent Cozic
01470e8d3b Android release v1.0.141 2018-10-03 08:30:19 +01:00
Laurent Cozic
bda2fe6717 Merge branch 'master' of github.com:laurent22/joplin 2018-10-03 08:19:15 +01:00
Laurent Cozic
d1f4c5be18 Disable non-working ShareExtension on iOS 2018-10-03 08:17:37 +01:00
Laurent Cozic
377adea51d Android release v1.0.140 2018-10-02 18:19:27 +01:00
Laurent Cozic
cda3d20834 Android release v1.0.138 2018-10-02 18:10:37 +01:00
Laurent Cozic
d11870b1eb Trying to get RN 0.57.1 to work 2018-10-02 19:02:38 +00:00
Laurent Cozic
53bda3eea7 Trying RN 0.57.1 upgrade 2018-10-02 18:57:31 +00:00
Laurent Cozic
30165e8d6a Electron: Fixes #798: Enable Select All shortcut in macOS 2018-10-02 17:45:39 +01:00
Laurent Cozic
2202eb6570 Merge branch 'master' of github.com:laurent22/joplin 2018-10-01 20:55:52 +01:00
Laurent Cozic
720927f488 All: Fixes #832: Enex import: Don't add extra line breaks at the beginning of list item when it contains a block element 2018-10-01 20:55:24 +01:00
Helmut K. C. Tessarek
2858c0fce0 CLI v1.0.116 2018-09-30 18:28:54 -04:00
Laurent Cozic
36c3521f40 Update website 2018-09-30 21:15:21 +01:00
Laurent Cozic
98a3b99d17 Electron release v1.0.111 2018-09-30 20:45:03 +01:00
Laurent Cozic
95a06c4531 All: Fixes #818: Use sort options when displayed tagged notes 2018-09-30 20:43:46 +01:00
Laurent Cozic
6ea77b36ce Electron: Resolves #820: Allow dragging and dropping a note in another note to create a link 2018-09-30 20:15:30 +01:00
Laurent Cozic
0cd7ebf9d3 Electron: Fixes resources being incorrectly auto-deleted when inside an IMG tag 2018-09-30 19:24:02 +01:00
Laurent Cozic
a816498fc6 Android release v1.0.135 2018-09-30 10:52:30 +01:00
Laurent Cozic
549c1a6767 Android: Make camera features optional to, maybe, enable Chromebooks 2018-09-30 10:48:10 +01:00
Laurent Cozic
f87d1f11b0 API: Allow downloading a resource data 2018-09-30 10:15:46 +01:00
Laurent Cozic
fb913bc33c Mobile: Fixes #654: Fixed auto-title logic so that set title doesn't get overwritten in some conditions for new notes 2018-09-29 15:57:18 +01:00
Laurent Cozic
53d7a51cb0 Removed API doc from translations and updated FR translation 2018-09-29 13:29:07 +01:00
Laurent Cozic
12da48c756 Electron release v1.0.110 2018-09-29 13:17:48 +01:00
Laurent Cozic
a0a6bdb684 Update website 2018-09-29 13:15:36 +01:00
Laurent Cozic
eb4aa2c026 API: Added more calls 2018-09-29 12:54:44 +01:00
Laurent Cozic
a9e789f845 Clipper: Fixed screenshot text box label colour 2018-09-29 12:53:16 +01:00
Laurent Cozic
89b76918bd Merge branch 'master' of github.com:laurent22/joplin 2018-09-28 21:04:23 +01:00
Laurent Cozic
e98575643c API: Added documentation generator and built documentation 2018-09-28 21:03:28 +01:00
Laurent Cozic
7c9e7743f1 Merge pull request #825 from sensor-freak/patch-2
Fixed some german translations
2018-09-28 20:58:32 +01:00
sensor-freak
435aa4845b Fixed some german translations 2018-09-28 21:43:17 +02:00
Laurent Cozic
9841488ce4 Updated translations 2018-09-28 19:25:56 +01:00
Laurent Cozic
9c907989a5 Merge branch 'master' of github.com:laurent22/joplin 2018-09-28 19:25:18 +01:00
Laurent Cozic
f684d8e59a Merge pull request #824 from sensor-freak/patch-1
Update de_DE.json
2018-09-28 19:25:05 +01:00
Laurent Cozic
a1ad6c9712 API: Added more API calls 2018-09-28 19:24:57 +01:00
Laurent Cozic
b6ca3090df Merge pull request #822 from foxmask/master
Forbidden is 403 - typo :)
2018-09-28 19:23:48 +01:00
Helmut K. C. Tessarek
ff2d793fbb CLI v1.0.115 2018-09-28 14:08:21 -04:00
sensor-freak
fcfb7f1111 Update de_DE.json
Fix and add some german translations
2018-09-28 07:39:13 +02:00
FoxMaSk
6125cde223 Forbidden is 403 - typo :) 2018-09-27 21:38:16 +02:00
Laurent Cozic
c83391e624 Update website 2018-09-27 19:02:01 +01:00
Laurent Cozic
a3a818ea74 Clipper release v1.0.7 2018-09-27 19:00:04 +01:00
Laurent Cozic
54a4965503 Android release v1.0.133 2018-09-27 18:54:14 +01:00
Laurent Cozic
2233d88c01 Electron release v1.0.109 2018-09-27 18:50:38 +01:00
Laurent Cozic
9680ab74a3 All: Allow loading image resources in IMG html tags 2018-09-27 18:42:34 +01:00
Laurent Cozic
ef711af5b5 Api: Added method to get notes 2018-09-27 18:35:10 +00:00
Laurent Cozic
8a619e4b8b All: Refactored REST API to make it testable and to allow further extension 2018-09-27 09:14:05 +01:00
Laurent Cozic
bc09d2c640 Clipper: Fixes #817: Added support for PICTURE tags, which will fix issues with certain pages from which images were not being imported 2018-09-25 21:19:54 +01:00
Laurent Cozic
f82dfde6f4 Clipper: Fixed importing certain images with sources that contain brackets 2018-09-24 20:15:23 +01:00
Laurent Cozic
312c7f2d27 Electron: Fixed regression following security fix - links were being opened inside Joplin instead of in external browser 2018-09-24 20:14:21 +01:00
Laurent Cozic
953cc327c6 Electron: Fixes #805: Fixed app freezing when opening note in external editor and then creating new note 2018-09-24 07:10:00 +01:00
Laurent Cozic
14cff96713 Mobile: Fix potential crash when going back to OneDrive login screen 2018-09-23 20:45:34 +01:00
Laurent Cozic
34b9af2ce0 Electron: Fixes #802: Scale note text correctly when using zoom 2018-09-23 19:48:50 +01:00
Laurent Cozic
6a6ee280c3 All: Fixes #801: Replaced freegeoip which is no longer free with ip-api 2018-09-23 19:45:13 +01:00
Laurent Cozic
861387707a CLI: Fixes #795: Display tree of notebooks correctly 2018-09-23 19:33:44 +01:00
Laurent Cozic
830e665366 Updated translation 2018-09-23 19:15:49 +01:00
Laurent Cozic
f14ae68ea0 Merge pull request #812 from ikunya/update-japo
Update Japanese Translations.
2018-09-23 19:15:16 +01:00
Laurent Cozic
c7084bf27e Updated French translation 2018-09-23 19:15:04 +01:00
Laurent Cozic
fc8ffcbe46 Clipper: Fixes #672: Make sure selected notebook is saved and restored correctly 2018-09-23 18:44:39 +01:00
Laurent Cozic
77f089654e Clipper: Resolves #681: Allow adding tags from Web Clipper 2018-09-23 18:03:11 +01:00
Laurent Cozic
e7a12bb0dd Clipper: Fixes #809: Saves full URL with note, including query parameters 2018-09-22 17:41:09 +01:00
AWASHIRO Ikuya
22fe3a4e44 Update Japanese Translations. 2018-09-22 23:34:07 +09:00
Laurent Cozic
afb8b92528 Clipper: Fixed init sequence 2018-09-22 11:21:39 +01:00
Laurent Cozic
5178f99100 Electron release v1.0.108 2018-09-21 18:20:34 +01:00
Laurent Cozic
72af564382 Electron: Fixed security issue by enabling contextIsolation and proxying IPC messages via preload script 2018-09-21 18:20:06 +01:00
Laurent Cozic
0a2b83998c Merge pull request #811 from mvonmaltitz/master
Fix ubuntu install script
2018-09-21 12:00:54 +01:00
Marcel von Maltitz
73e79213dc Fix ubuntu install script
If no former version is present, the script fails since the rm commands do not succeed.
Added -f to rm in order to ignore non-existent directories to be deleted.
2018-09-21 12:55:01 +02:00
Laurent Cozic
e31ffc9474 Update website 2018-09-16 20:51:28 +01:00
Laurent Cozic
fdb8706a5f Android release v1.0.132 2018-09-16 20:12:56 +01:00
Laurent Cozic
4c0262bd82 Electron release v1.0.107 2018-09-16 20:09:07 +01:00
Laurent Cozic
3b2dcb37a6 CLI: Updated Readme 2018-09-16 20:03:29 +01:00
Laurent Cozic
46a3b020a6 Merge pull request #784 from Kriechi/cli-decrypt
CLI: improve e2ee decrypt command
2018-09-16 20:01:09 +01:00
Laurent Cozic
8373392e99 Merge pull request #793 from tanrax/patch-4
Added comments and information texts
2018-09-16 19:53:33 +01:00
Laurent Cozic
695c2623c2 Added Romanian translation 2018-09-16 19:53:06 +01:00
Laurent Cozic
979e7f2486 Merge pull request #796 from cdorin93/ro-translation-added
Romanian translation added
2018-09-16 19:50:03 +01:00
Laurent Cozic
e7a9f630ec All: Fixes #799: Handle restricted_content error for Dropbox 2018-09-16 19:49:07 +01:00
Laurent Cozic
4e8372174b Electron: Resolves #755: Added note properties dialog box to view and edit created time, updated time, source URL and geolocation 2018-09-16 19:37:31 +01:00
cdorin93
1b8912d7e9 Romanian translation added 2018-09-15 22:49:40 +02:00
Andros Fenollosa
8c3669588b Added comments and information texts
Comments, presentation title, final informative text have been added and temporary variable is deleted.
2018-09-14 19:03:09 +02:00
Laurent Cozic
1b784fe3b0 Merge branch 'master' of github.com:laurent22/joplin 2018-09-13 20:53:39 +01:00
Laurent Cozic
5ab1d8dfd6 All: Resolves #781: Allow creating notebooks with duplicate titles to allow two notebooks with same name to exist under different parents 2018-09-13 20:53:31 +01:00
Laurent Cozic
cda8b95bfa Merge pull request #768 from sebastienjust/master
Notebooks list is sorted alphabetically as default
2018-09-13 19:56:57 +01:00
Laurent Cozic
9664842b1a Update Chinese translation 2018-09-13 19:48:00 +01:00
Laurent Cozic
09836e1d34 Merge branch 'master' of github.com:laurent22/joplin 2018-09-13 19:46:12 +01:00
Laurent Cozic
8974e20c7f Update readme 2018-09-13 19:46:06 +01:00
Laurent Cozic
761a49803e Merge pull request #775 from tanrax/patch-1
Script to install on Ubuntu with Gnome Shell
2018-09-13 19:45:24 +01:00
Laurent Cozic
a40028f0c0 Merge pull request #776 from tanrax/patch-2
Create install_ubuntu.sh
2018-09-13 19:45:14 +01:00
Laurent Cozic
d4fca7e313 Merge pull request #787 from tessus/external-storage
allow the app to be installed on external storage (SD card)
2018-09-13 19:42:34 +01:00
Laurent Cozic
6748d4d825 Merge branch 'master' of github.com:laurent22/joplin 2018-09-13 19:31:18 +01:00
Laurent Cozic
0a5ad1d628 Electron: Fixes #710: Don't unwatch file when it is temporarily deleted 2018-09-13 19:29:48 +01:00
Laurent Cozic
4080958e10 Merge pull request #790 from chaifeng/zh_CN
Update zh_CN.po
2018-09-12 23:15:32 +01:00
Chai Feng
95c4a717e3 Update zh_CN.po 2018-09-12 09:13:10 +08:00
Helmut K. C. Tessarek
c5b9353105 fix whitespace errors (spaces to tabs) 2018-09-10 20:06:38 -04:00
Helmut K. C. Tessarek
17595f7ceb allow the app to be installed on external storage (SD card) 2018-09-10 20:04:54 -04:00
Andros Fenollosa
dcf78e8a06 Prevent the folder from being deleted 2018-09-10 22:57:50 +02:00
Andros Fenollosa
de0c54c3c3 Add set -e 2018-09-10 22:54:01 +02:00
Laurent Cozic
38970e9a52 Merge branch 'master' of github.com:laurent22/joplin 2018-09-10 19:19:42 +01:00
Laurent Cozic
563f43168b All: Fix #764: Fix equation tag positioning 2018-09-10 19:19:33 +01:00
Thomas Kriechbaumer
6e235605ed CLI: improve e2ee decrypt command 2018-09-10 18:39:19 +02:00
Laurent Cozic
0749e0b675 Merge pull request #777 from tanrax/patch-3
Add fastmail WebDAV synchronisation list
2018-09-09 20:54:30 +01:00
Laurent Cozic
756f3e627c All: Fixes #718: Allow recursively importing Markdown folder 2018-09-09 20:32:23 +01:00
Andros Fenollosa
4b39ed42b1 Add fastmail WebDAV synchronisation list 2018-09-09 21:16:42 +02:00
Andros Fenollosa
abe85ca4bd Update README.md 2018-09-09 21:11:42 +02:00
Andros Fenollosa
a559565ace Create install_ubuntu.sh 2018-09-09 21:08:23 +02:00
Laurent Cozic
d35e3163ca Update translations 2018-09-09 20:05:36 +01:00
Laurent Cozic
f22ad85681 Removed incorrectly duplicated translation 2018-09-09 20:02:08 +01:00
Andros Fenollosa
727bdaeea4 Update README.md 2018-09-09 20:58:30 +02:00
Laurent Cozic
42f7764eed Merge pull request #773 from Vistaus/master
Added Dutch (Netherlands) translation
2018-09-09 19:54:20 +01:00
Andros Fenollosa
1fbc1073ca Script to install on Ubuntu with Gnome Shell
Add script to install on Ubuntu or Debian with Gnome Shell Icon
2018-09-09 20:47:57 +02:00
Heimen Stoffels
66b683e5e7 Added Dutch (Netherlands) translation 2018-09-09 18:26:58 +02:00
Laurent Cozic
7d1f61e47b Electron release v1.0.106 2018-09-08 15:47:15 +01:00
Laurent Cozic
643e5a6a2a Electron: Resolves #714: Allow starting application minimised in the tray icon 2018-09-06 18:56:23 +01:00
Laurent Cozic
a1e7e29279 All: Fixes #709: Now that HTML is supported in notes, remove BR tag replacement hack. 2018-09-06 18:40:05 +01:00
Laurent Cozic
abf6c3f3f1 Electron: Fixes #697: Focus search text input after clearing search 2018-09-06 18:37:39 +01:00
Laurent Cozic
32c81ad8c2 All: Fixes #657: Disallow giving name of existing tag to another tag 2018-09-06 18:33:21 +01:00
Sebastien Just
0f461c4caa #206 Notebooks list defaults to alphabetical order 2018-09-06 12:26:35 +02:00
Sebastien Just
57ed718993 #206 Notebooks list defaults to alphabetical order 2018-09-06 12:22:09 +02:00
Laurent Cozic
ef1ae63233 Electron: Fix #759: Add border around code block when exporting to PDF 2018-09-05 14:59:33 +01:00
Laurent Cozic
81ac200cc0 All: Resolves #761: Highlight single tick code segments 2018-09-05 14:51:50 +01:00
Laurent Cozic
3a2d62f6c7 Update website 2018-09-05 12:30:37 +01:00
Laurent Cozic
7f80f67fd6 CLI v1.0.114 2018-09-05 12:30:09 +01:00
Laurent Cozic
cebd8de77a Android release v1.0.131 2018-09-05 12:19:53 +01:00
Laurent Cozic
417218fc34 Electron release v1.0.105 2018-09-05 12:11:56 +01:00
Laurent Cozic
29586437c2 Merge pull request #719 from gufertum/tag-list-long-op
added support for 'tag list -l [tag]' to show long format as for 'not…
2018-09-05 12:11:03 +01:00
Thomas Schädler
f51d0ad914 fixed indention (again, this time for real) 2018-09-05 12:46:49 +02:00
Laurent Cozic
35294b5f97 Electron: Resolves #679: Drag a note on a tag to associate the tag. 2018-09-05 11:43:03 +01:00
Laurent Cozic
758562cff9 Electron: Fixes #113: Upgraded Ace Editor to try to fix Korean input issue (to be confirmed). 2018-09-05 11:35:40 +01:00
Laurent Cozic
da0678c6fe Merge pull request #726 from tfinnberg/enable_file-urls_in_notes_html_view
enable file-URLs in note html view
2018-09-05 11:23:12 +01:00
Laurent Cozic
afe4fd70cc Update FAQ 2018-09-05 11:22:04 +01:00
Laurent Cozic
4cef383fe7 Added warning when changing WebDAV URL 2018-09-05 11:20:26 +01:00
Laurent Cozic
b58c30889e Updated FAQ and contribution guide 2018-09-05 11:17:47 +01:00
Laurent Cozic
1561c0e4d7 Merge pull request #756 from tessus/cli-correct-date-time-ls
show correct date/time for cli command `ls`
2018-09-05 10:56:44 +01:00
Thomas Schädler
32b11c15a4 fixed indention 2018-09-05 08:45:18 +02:00
Helmut K. C. Tessarek
5e06efc1b9 show correct date/time for cli command ls
closes #729
2018-09-04 18:03:21 -04:00
Helmut K. C. Tessarek
43bd88703c fix 'No newline at end of file' and whitespace errors 2018-09-04 18:00:12 -04:00
Laurent Cozic
cdd70230af Electron: Fixes #663: Fixed copy, cut and paste context menu on text editor 2018-09-04 18:20:41 +01:00
Laurent Cozic
eaf3eef2d3 All: Resolves #734: Allow exporting to a hierarchy of Markdown files, and fixed a few issues related to exporting notebooks 2018-09-04 11:59:09 +01:00
Laurent Cozic
81ec8eaf83 Merge pull request #757 from pensierocrea/patch-1
Update it_IT.po
2018-09-04 11:10:17 +01:00
Laurent Cozic
23f7e350c6 Redirect to app logger 2018-09-04 11:08:55 +01:00
Laurent Cozic
cea368cd3f Fix sharing text when no folder exists 2018-09-04 11:08:08 +01:00
Laurent Cozic
50c8f2ae61 Merge pull request #689 from CalebJohn/android-sharing
Add text sharing on android
2018-09-04 11:06:08 +01:00
pensierocrea
ed0ecababb Update it_IT.po
Hi,
i'm new to github... i hope this help and i've not done some mistake
i think that:
"notebook" should be named "taccuini" in italian like evenote does 
"to-do" can be translated in "Cose-da-fare" not in "attività" that means "activity/activities"

now... i've also a feature request to improve the user interaction with the sidebar and to focus more on the "to-do" stuff
where i can send/post my little suggestion?

thank you
2018-09-04 11:12:19 +02:00
Thomas Schädler
72aa4c40a5 added correct time format method (using user properties)
cleanup (spaces -> tabs, consistent line endings)
2018-09-03 22:18:15 +02:00
Laurent Cozic
4f6784e2e5 Upgraded Katex to support tag 2018-09-03 16:08:56 +01:00
Laurent Cozic
01f015a54f Update translations 2018-09-03 15:11:12 +01:00
Laurent Cozic
806acad22a Fix Sweden flag 2018-09-03 12:16:35 +01:00
Laurent Cozic
1d322d8a39 Fix Korean flag 2018-09-03 12:13:59 +01:00
Laurent Cozic
aef94e6950 Updated translations 2018-09-03 12:08:50 +01:00
Laurent Cozic
456fcec334 Merge branch 'master' of github.com:laurent22/joplin 2018-09-03 12:08:02 +01:00
Laurent Cozic
3b6937c2f0 Merge pull request #746 from jony0008/master
Add Swedish translation
2018-09-03 12:01:46 +01:00
Laurent Cozic
7cdd1d41c1 Merge pull request #728 from fmrtn/master
Spanish translation updated
2018-09-03 12:00:33 +01:00
Laurent Cozic
1fc535a740 Merge pull request #706 from tessus/fix-421
use correct date/time format for sync time - fix for #421
2018-09-03 11:56:04 +01:00
Laurent Cozic
033b37077a Merge pull request #705 from tessus/res-icon-j
use the Joplin icon for internal links
2018-09-03 11:55:25 +01:00
Laurent Cozic
07f6a4a08b Merge pull request #676 from RaphaelKimmig/electron_focus_new_notes_only
focus NoteText only when creating new notes
2018-09-03 11:53:03 +01:00
Laurent Cozic
8c1b592a51 Updated translations 2018-09-03 11:49:41 +01:00
Jony
9460f7a17a Add Swedish translation 2018-08-30 19:48:41 +02:00
Fernando
106260ed69 Spanish translation updated 2018-08-20 02:43:18 +02:00
tfinnberg
123162e946 enable file-URLs in note html view 2018-08-18 11:14:34 +02:00
Thomas Schädler
54e81966e5 added support for 'tag list -l [tag]' to show long format as for 'notebook' lists using 'ls -l' 2018-08-13 23:28:19 +02:00
Helmut K. C. Tessarek
9bf6ab60bb use correct date/time format for sync time
fixes #421
2018-08-01 17:22:24 -04:00
Helmut K. C. Tessarek
4f0ff3cdfc fix whitespace errors 2018-08-01 17:05:09 -04:00
Helmut K. C. Tessarek
47cfaaa5ab replace the resource icon (for internal links) with the Joplin icon (from ForkAwesome) 2018-07-31 23:04:33 -04:00
Helmut K. C. Tessarek
1f49788f21 fix 'No newline at end of file' 2018-07-31 23:00:28 -04:00
Laurent Cozic
7e4cf9aeda All: Resolves #678: Added Korean translation 2018-07-25 18:01:56 +02:00
Laurent Cozic
4b6964b683 CLI v1.0.113 2018-07-25 17:59:00 +02:00
Caleb John
3caf398021 refresh notes list on AppState Changes
- remove mistaken console.log calls in root.js
2018-07-20 15:59:55 +02:00
Caleb John
8840631266 Add simple sharing facilities for Android
- react code should be cross platform but support needs to be added
    for ios
  - only shares plain text notes for now
2018-07-20 11:04:25 +02:00
Raphael Kimmig
c4411bb895 focus NoteText only when creating new notes
This changes the behaviour so that loading a note does automatically
focus the note text/title only when a new note is being created.

This reduces accidental edits and is in line with other note taking
applications such as Simplenote, Evernote and Apple Notes.
2018-07-10 08:35:21 +02:00
Laurent Cozic
f63668350b CLI v1.0.110 2018-06-30 19:58:36 +01:00
Laurent Cozic
3fc54d7ffd CLI: Updating SQLite to support Node 10 2018-06-30 19:57:49 +01:00
Laurent Cozic
2c6c20f44f Mobile: Resolves #350: Open non-image resources in external application 2018-06-30 19:45:21 +01:00
Laurent Cozic
08ee939951 Electron: Resolves #594: Enable support for SVG graphics 2018-06-30 19:05:45 +01:00
Laurent Cozic
463b1441d3 Updated DE translation 2018-06-30 16:20:22 +01:00
Laurent Cozic
6754d4ee89 Electron: Fixes #653: Don't detect horizontal rule as bullet list item 2018-06-30 16:15:44 +01:00
Laurent Cozic
d5d0732bf3 Clipper: Upgrade joplin-turndonw to fix import of literal HTML code 2018-06-30 16:12:01 +01:00
Laurent Cozic
d27cbaa663 Electron: Improve speed of loading notes that include many resources, and prevent UI from freezing 2018-06-29 18:51:50 +00:00
Laurent Cozic
70adf10f2e Clipper: Skip noscript elements 2018-06-29 18:45:33 +00:00
Laurent Cozic
e75417d26e Tools: Automate updating Git repo when creating new Clipper release 2018-06-28 22:16:15 +01:00
Laurent Cozic
2ded983828 Clipper: Resolves #641: Allow clipping selected part of the page 2018-06-28 22:01:55 +01:00
Laurent Cozic
0c708f766b All: Resolves #427: Import source-url from Enex files 2018-06-28 21:36:32 +01:00
Laurent Cozic
a801f8d8ed Update website 2018-06-28 21:27:54 +01:00
Laurent Cozic
26fc26c9fe CLI v1.0.109 2018-06-28 20:49:26 +01:00
Laurent Cozic
df4c07d204 CLI: Added way to decrypt string 2018-06-28 20:48:39 +01:00
Laurent Cozic
cf565d1563 Android release v1.0.129 2018-06-27 22:02:15 +01:00
Laurent Cozic
6b425cf543 Electron release v1.0.104 2018-06-27 22:00:38 +01:00
Laurent Cozic
6188e7a0fa Updated translations 2018-06-27 22:00:02 +01:00
Laurent Cozic
310afb0ad6 Electron: Resolves #612: Allow duplicating a note 2018-06-27 21:45:31 +01:00
Laurent Cozic
7d7e1e1637 Electron: Resolves #647: Allow specifying text editor path and arguments in setting 2018-06-27 21:34:41 +01:00
Laurent Cozic
424c8a2723 Clipper: Support 'author' property 2018-06-27 20:14:20 +01:00
Laurent Cozic
187fb1b85d Electron: Resolves #619: Context menu to cut, copy and paste. Also added menu to copy link in web view 2018-06-26 00:52:46 +01:00
Laurent Cozic
595fd7a9aa All: Resolves #644: Added support for .markdown extension when importing files 2018-06-26 00:07:53 +01:00
Laurent Cozic
0027cb9036 All: Fixes #646: Mentioned that TLS settings must be saved before checking sync config 2018-06-25 23:54:28 +01:00
Laurent Cozic
db6878b978 Electron: Fixes #639: Make sure text wraps when printing or exporting as PDF 2018-06-25 23:32:23 +01:00
Laurent Cozic
1c78722573 CLI: Upgraded Turndown plugins 2018-06-25 18:19:27 +01:00
Laurent Cozic
fea83e28c4 All: Optimised encryption and decryption of items so that it doesn't freeze the UI, especially on mobile 2018-06-25 18:14:57 +01:00
Laurent Cozic
84adf64271 Electron: Set PDF default file name 2018-06-22 18:36:15 +00:00
Laurent Cozic
74e2b0d15d Electron: Fixes #634: Press ESC to dismiss dialog in non-English languages 2018-06-22 18:31:55 +00:00
Laurent Cozic
df302206dd Electron: Allow HTML in Markdown documents in a secure way 2018-06-22 18:18:15 +00:00
Laurent Cozic
6d8941c005 Update website 2018-06-21 22:14:47 +01:00
Laurent Cozic
971b20062f Electron release v1.0.103 2018-06-21 19:13:10 +01:00
Laurent Cozic
936f334b61 Electron: Remove 'New notebook' shortcut 2018-06-21 19:12:08 +01:00
Laurent Cozic
7e3a290939 Merge pull request #628 from Abijeet/master
Adds a shortcut to insert the date and time.
2018-06-21 19:09:36 +01:00
Laurent Cozic
c6466a780e Android release v1.0.128 2018-06-21 19:08:59 +01:00
Laurent Cozic
43774ad3fb Electron release v1.0.102 2018-06-21 19:05:58 +01:00
Laurent Cozic
b3ba5b7747 All: Revert #554 to try to fix #624: WebDAV error when syncing with SeaFile 2018-06-21 19:00:20 +01:00
Laurent Cozic
599f4ccef4 Electron: Fixes #626: Auto-completion for indented items 2018-06-21 18:53:42 +01:00
Laurent Cozic
a67600d264 Updated Chinese and French translation 2018-06-21 18:04:10 +01:00
Laurent Cozic
ebf4c89ef0 All: Fixes #343, Fixes #191: Added options to specify custom TLS certificates 2018-06-20 01:18:58 +01:00
Laurent Cozic
aa7da784fc All: Fixes #343, Fixes #191: Added options to ignore TLS cert errors to allow self-signed certificates on desktop and CLI 2018-06-20 00:28:50 +01:00
Laurent Cozic
617ed42d8c Update website 2018-06-18 19:08:13 +01:00
Laurent Cozic
5848e7d90d Doc: Fixed image 2018-06-18 19:08:07 +01:00
Abijeet Patro
01d032261c Merge branch 'master' into master 2018-06-18 23:32:10 +05:30
Laurent Cozic
54d06646aa Update website 2018-06-18 19:01:52 +01:00
Laurent Cozic
81da46035a Doc: Fixed image 2018-06-18 19:01:42 +01:00
Laurent Cozic
74d0f75802 Update website 2018-06-18 19:00:07 +01:00
Laurent Cozic
f25a352dcb Doc: Mentioned Web Clipper 2018-06-18 18:59:54 +01:00
Laurent Cozic
21ef8da45f Clipper: Completed doc and simplified logic to get clipper port to make it easier to use by third-party 2018-06-18 18:54:48 +01:00
Laurent Cozic
1f3a1c49df Electron: Resolves #611: Allow opening and editing note in external editor 2018-06-18 18:56:07 +00:00
Laurent Cozic
a8b58aaec3 All: Fixes #632: Handle restricted_content error in Dropbox 2018-06-18 08:47:51 +01:00
Laurent Cozic
44f9b35d93 Update website 2018-06-17 19:35:25 +01:00
Laurent Cozic
711af9beed Electron release v1.0.101 2018-06-17 19:12:28 +01:00
Laurent Cozic
971339ca9a Electron: Fixes #630: PDF export in context menu 2018-06-17 17:12:28 +01:00
Laurent Cozic
f5a72ffbaf All: Fixes #593: Resource should not be auto-deleted if they've never been linked to any note 2018-06-17 16:59:06 +01:00
Laurent Cozic
cf4331c5af Electron: Fixes #623: Improved handling of text selection and fixed infinite loop 2018-06-17 02:44:37 +01:00
Abijeet
07b85388fc Adds a shortcut to insert the date and time.
Closes #521
2018-06-16 20:46:27 +05:30
Laurent Cozic
553b086ba2 Update website 2018-06-15 19:56:17 +01:00
Laurent Cozic
ff89537899 Update website 2018-06-15 19:28:58 +01:00
Laurent Cozic
f20792889a Removed Twitter and FB share buttons 2018-06-15 19:28:35 +01:00
Laurent Cozic
ee22a7ff73 Update website 2018-06-15 18:23:23 +01:00
Laurent Cozic
4fc4353859 Update translation 2018-06-15 18:23:15 +01:00
Laurent Cozic
f4f9e25e6b Update website 2018-06-15 18:14:55 +01:00
Laurent Cozic
e54f9934b5 Fixed Catalan flag 2018-06-15 18:14:31 +01:00
Laurent Cozic
f599ae065a CLI v1.0.108 2018-06-14 22:42:43 +01:00
Laurent Cozic
5bd9bf6a4e Update dependencies 2018-06-14 22:42:15 +01:00
Laurent Cozic
cb9e8d4f76 Update website 2018-06-14 18:41:57 +01:00
Laurent Cozic
f64596672e Android release v1.0.127 2018-06-14 18:23:14 +01:00
Laurent Cozic
961150b2d3 Electron release v1.0.100 2018-06-14 18:19:48 +01:00
Laurent Cozic
0cd8e1cbc0 Updated translations 2018-06-14 18:18:48 +01:00
Laurent Cozic
b503aff5e9 Clipper: v1.0.5 2018-06-14 18:17:42 +01:00
Laurent Cozic
54bde47c67 Merge pull request #607 from jmontane/patch-1
Add Catalan Translation
2018-06-14 18:16:53 +01:00
Laurent Cozic
d345f8dc13 Clipper: Fixed extension being incorrectly detected as dev mode. Improved build process. 2018-06-14 18:11:22 +00:00
Laurent Cozic
979b0c0e78 Electron: Fixed prompt dialog font 2018-06-14 09:04:58 +01:00
Laurent Cozic
13525f3327 Electron: Switched icons to Fork Awesome 2018-06-14 09:02:01 +01:00
Laurent Cozic
5d9c2c0904 Electron: Added more toolbar icons and upgraded Font Awesome 2018-06-14 08:52:12 +01:00
Laurent Cozic
c748281d86 Update website 2018-06-13 19:22:49 +01:00
Laurent Cozic
fa619eba7c Added zh_TW translation 2018-06-13 19:22:18 +01:00
Laurent Cozic
17a75f7cf5 Electron: Made checkbox button more consistent 2018-06-13 17:53:41 +01:00
Joan Montané
a74cfbfb25 Add Catalan Translation 2018-06-12 14:46:51 +02:00
Laurent Cozic
af01fed950 Electron: Added hyperlink, checkbox and code options in toolbar 2018-06-12 00:12:06 +01:00
Laurent Cozic
218b446915 Electron: Added Bold and Italic toolbar buttons 2018-06-11 23:47:44 +01:00
Laurent Cozic
a68df18cd5 Updated website 2018-06-11 22:18:34 +01:00
Laurent Cozic
f79326b2d5 Updated website 2018-06-11 22:10:04 +01:00
Laurent Cozic
b08dcdfd90 Electron: Fixes #598: Allow opening any URL with a valid protocol in an external app 2018-06-11 00:35:01 +01:00
Laurent Cozic
ced14e578f Merge branch 'master' of github.com:laurent22/joplin 2018-06-11 00:24:44 +01:00
Laurent Cozic
0528c6e970 All: Try to display more info when there is a Dropbox API error 2018-06-11 00:24:29 +01:00
Laurent Cozic
df9c1e0aeb Electron: Fixes #570: Allow opening files by right-clicking on a resource in macOS 2018-06-11 00:08:57 +01:00
Laurent Cozic
b6619b41df All: Fixes #600: Improved resuming of long sync operations so that it doesn't needlessly re-download the items from the beginning 2018-06-10 23:16:27 +01:00
Laurent Cozic
ab9675544c All: Fixes #597: Also import sub-notebooks when importing JEX data 2018-06-10 19:15:40 +01:00
Laurent Cozic
0d9f703c75 Update DE translation 2018-06-10 17:44:00 +01:00
Laurent Cozic
f6ee5dd0e7 Mobile: Display decryption progress in side bar 2018-06-10 17:43:24 +01:00
Laurent Cozic
423d880b92 Merge branch 'master' of github.com:laurent22/joplin 2018-06-10 15:18:10 +01:00
Laurent Cozic
41017b9ab8 Update website 2018-06-10 15:18:07 +01:00
Laurent Cozic
e3314c859f Merge pull request #554 from bradmcl/master
Handle Nginx DAV PROPFIND responses correctly
2018-06-10 15:11:38 +01:00
bradmcl
8f794fdbc6 Search for 404 anywhere in return status 2018-06-10 10:04:52 -04:00
Laurent Cozic
80b0773618 Update website 2018-06-10 14:18:50 +01:00
Laurent Cozic
ac848241b9 Electron release v1.0.99 2018-06-10 14:03:17 +01:00
Laurent Cozic
b4432e2efc Removed snap support 2018-06-10 14:02:55 +01:00
Laurent Cozic
52f60a2cf6 Electron release v1.0.98 2018-06-10 13:52:11 +01:00
Laurent Cozic
03c8438050 Removed broken snap package 2018-06-10 13:45:05 +01:00
Laurent Cozic
0e1c36ccf1 Electron: Fixes #601: Linux app cannot start anymore due to build error 2018-06-10 12:37:12 +01:00
Laurent Cozic
1b2b68c485 Electron: Fixes #562: Disable power saving when syncing in Linux 2018-06-10 12:19:36 +01:00
Laurent Cozic
5c36f3e78a Update translations 2018-06-10 11:54:29 +01:00
Laurent Cozic
d4ec8ae823 Merge pull request #588 from zblesk/search-display-notebook
Displaying the notebook's name in the Note toolbar when search is act…
2018-06-10 11:50:08 +01:00
Laurent Cozic
449a70d840 Merge pull request #575 from foxmask/issue572
fix #572 - Translation submenu
2018-06-10 11:41:12 +01:00
zblesk
8375030135 Reverting previous change; using disabled button for the label. 2018-06-10 11:18:02 +02:00
Laurent Cozic
e9f938b0fb Electron: Fixes #560: Drag the note that was clicked, which might be different from the note that was selected 2018-06-10 01:27:20 +01:00
Laurent Cozic
b0e57a5990 All: Fixes #557: Handle ENEX am-pm date format 2018-06-10 01:19:24 +01:00
Laurent Cozic
f47610e6fd Update website 2018-06-09 20:23:52 +01:00
Laurent Cozic
c131cb9bb8 Electron release v1.0.97 2018-06-09 20:02:00 +01:00
Laurent Cozic
023f775bd2 Merge branch 'master' of github.com:laurent22/joplin 2018-06-09 20:00:35 +01:00
Laurent Cozic
1127eb6e09 Clipper: Fixes #573: Better handling of certain code blocks 2018-06-09 20:00:26 +01:00
Ladislav Benc
0d7437c7d2 Also showing the display when browsing tags. 2018-06-06 15:18:58 +02:00
Ladislav Benc
6dbc691973 Fixing locale and making the Notebook name no longer behave like a button. 2018-06-06 14:11:46 +02:00
Laurent Cozic
fe53200a3a Added link to NixNote info 2018-06-06 00:16:21 +01:00
zblesk
c7f61271a0 Displaying the notebook's name in the Note toolbar when search is active. 2018-06-05 10:27:07 +02:00
Laurent Cozic
b826e2d97b Clipper: Fixes #569: Make clipper service available on localhost only 2018-06-01 16:44:16 +01:00
Laurent Cozic
d8ad42b04a Clipper: Take zoom into account when clipping screenshot 2018-06-01 16:12:49 +01:00
Laurent Cozic
b3ca30b8b6 Merge branch 'master' of github.com:laurent22/joplin 2018-06-01 15:50:23 +01:00
Laurent Cozic
bdd9da3d22 Clipper: More consistent REST API 2018-06-01 15:50:11 +01:00
Laurent Cozic
eb43ddc701 Merge pull request #576 from zuphilip/patch-1
Update de_DE.po
2018-05-31 11:16:40 +01:00
Philipp Zumstein
f9c65a148f Update de_DE.po 2018-05-30 19:43:10 +02:00
FoxMaSk
bd0b9dff51 fix #572 - Translation submenu 2018-05-30 19:22:07 +02:00
Laurent Cozic
3822309657 All: Auto-delete resources only after 10 days to handle some edge cases 2018-05-27 12:50:12 +01:00
Laurent Cozic
ac2ec65c81 Update ES translation 2018-05-27 12:44:41 +01:00
Laurent Cozic
c0943f1776 Merge branch 'master' of github.com:laurent22/joplin 2018-05-27 12:44:09 +01:00
Laurent Cozic
5a2ab5fae7 Mobile: Fixes #565: Translate strings in dialog 2018-05-27 12:42:31 +01:00
bradmcl
281e36fde7 Update file-api-driver-webdav.js 2018-05-26 21:06:51 -04:00
Laurent Cozic
1af1c445c6 Merge pull request #564 from fmrtn/master
Updated Spanish translation
2018-05-26 22:31:20 +01:00
Fernando
e6d2e028ad Updated Spanish translation 2018-05-26 21:01:34 +02:00
Laurent Cozic
8d25b8075d Update website 2018-05-26 17:37:17 +01:00
Laurent Cozic
58201fd6c3 Clipper: Disable incorrect detection of dev mode for Firefox extension 2018-05-26 17:37:03 +01:00
Laurent Cozic
2b624a9aed Electron release v1.0.96 2018-05-26 17:25:15 +01:00
Laurent Cozic
792fd7c50d Update translations 2018-05-26 17:25:11 +01:00
Laurent Cozic
90d37a15bd Electron: Added link to Firefox extension 2018-05-26 17:13:27 +01:00
Laurent Cozic
ef57ee803f Clipper: Fixes to get it working in Firefox 2018-05-26 16:28:55 +01:00
Laurent Cozic
e2bfb74895 Clipper: Minor improvement to screenshot usability 2018-05-26 15:53:50 +01:00
Laurent Cozic
89b486a3ee Clipper: Allow selecting a folder and fixed screenshot taking issue 2018-05-26 15:46:57 +01:00
Laurent Cozic
d6c6ef20d4 Update website 2018-05-26 12:58:44 +01:00
Laurent Cozic
6cb6e9541f Clipper: started doc 2018-05-26 12:57:14 +01:00
Laurent Cozic
a8acecb703 Clipper: Display server status in popup 2018-05-26 12:17:41 +01:00
Laurent Cozic
0938297250 Clipper: Make dev icon look different and run dev server on different port 2018-05-26 11:18:54 +01:00
Laurent Cozic
a2da2f681c Update website 2018-05-25 14:04:48 +01:00
Laurent Cozic
32477e901d Electron release v1.0.95 2018-05-25 13:31:42 +01:00
Laurent Cozic
1841c4dc11 Merge branch 'master' of github.com:laurent22/joplin 2018-05-25 13:31:27 +01:00
Laurent Cozic
f81dce3321 Merge branch 'clipper' 2018-05-25 13:31:15 +01:00
Laurent Cozic
e15f84716a Clipper: Added Clipper config screen and improved server 2018-05-25 13:30:27 +01:00
Laurent Cozic
d11ecd8fac Clipper: Better handling of clipper server detection 2018-05-25 11:18:47 +01:00
Laurent Cozic
012e70d668 Clipper: Dynamically allocate and detect clipper service port 2018-05-25 11:08:22 +01:00
Laurent Cozic
264ee4f319 Clipper: Support clipping screenshots 2018-05-25 08:51:54 +01:00
Laurent Cozic
4640d6d6e3 Clipper: Made extension compatible with Chrome and started screenshot clipping 2018-05-24 18:32:30 +01:00
Laurent Cozic
9db9d98419 Clipper: Skip style section when importing HTML 2018-05-24 13:32:43 +01:00
Laurent Cozic
f79d7b9626 Clipper: Set source URL and fixed issues with tables and urls 2018-05-24 12:44:13 +01:00
Laurent Cozic
a8da469523 Clipper: Improved download of images and conversion to resources 2018-05-23 14:25:59 +01:00
Laurent Cozic
3c5eb99c59 Clipper: Download images and convert them to resources 2018-05-23 12:14:38 +01:00
bradmcl
8dc14516d6 Handle Nginx DAV PROPFIND responses correctly
Parse the d:status field for d:propstat sections within a d:multistatus response to a DAV PROPFIND because Nginx returns 404 statuses wrapped in an overall 207
2018-05-22 21:08:14 -04:00
Laurent Cozic
84efc6a04e ios-v10.0.24 2018-05-22 16:09:25 +01:00
Laurent Cozic
86e3038cfe Mobile: Fixes #531: Get WebDAV to work with certain servers that require a trailing slash on directories 2018-05-22 15:55:38 +01:00
Laurent Cozic
6898b9ca4c All: Identify another Dropbox missing auth error, to allow resetting the token 2018-05-22 15:03:55 +01:00
Laurent Cozic
9eb62920f7 Update SyncTargetDropbox.js 2018-05-22 15:02:35 +01:00
Laurent Cozic
7cf267254f Clipper: Improved Html To Md conversion 2018-05-22 00:54:23 +01:00
Laurent Cozic
5a0e3cbbf2 ios-v10.0.23 2018-05-21 23:02:47 +01:00
Laurent Cozic
4b376ec5c2 Update website 2018-05-21 21:53:13 +01:00
Laurent Cozic
92b71d3eb2 Electron release v1.0.94 2018-05-21 20:17:13 +01:00
Laurent Cozic
c32d7de7c4 Updated French transaltion 2018-05-21 20:17:00 +01:00
Laurent Cozic
c83a61d45d Android: Resolves #538 (kind of): Added info to ask user to set app permissions 2018-05-21 17:24:09 +01:00
Laurent Cozic
429f2d5aab All: Fixes #536: Allow changing sync target file path 2018-05-21 16:26:01 +01:00
Laurent Cozic
ed70cf571c Electron: partially resolves #532: Better handling of Dropbox error message 2018-05-21 16:24:57 +01:00
Laurent Cozic
fd77671575 Electron: Fixes #535: Note preview was not always updated when it should 2018-05-21 15:29:35 +01:00
Laurent Cozic
9d915a916e Fixed test 2018-05-21 15:21:08 +01:00
Laurent Cozic
acb90935c7 Fixed tests 2018-05-20 13:33:26 +01:00
Laurent Cozic
6301ba0a12 Electron: Fixes #527: Remove empty section separators from menus 2018-05-20 13:01:07 +01:00
Laurent Cozic
44e1245416 Electron: Fixes #528: Set translation in bridge functions too 2018-05-20 12:54:42 +01:00
Laurent Cozic
6527d9db83 Update translation 2018-05-20 12:31:54 +01:00
Laurent Cozic
2bcaf62a2f Merge pull request #543 from Abijeet/tag-rename
Adds functionality to allow for renaming of tags.
2018-05-20 12:24:15 +01:00
Laurent Cozic
2db3998f11 Merge pull request #540 from Abijeet/master
Added styles to fix margin bottom for  nested lists
2018-05-20 12:24:01 +01:00
Laurent Cozic
04724c58d1 All: Fixes #491: Handle non-standard ports and better handling of fetchBlob errors 2018-05-20 12:20:15 +01:00
Laurent Cozic
7ed9c2770c Clipper: Improved UI and integration with main app 2018-05-20 10:19:59 +01:00
Abijeet
48883bfa13 Adds functionality to allow for renaming of tags.
Towards #374

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-20 13:09:32 +05:30
Abijeet
b6d9e695d1 Added styles to fix margin bottom for nested lists
Fixes #482

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-19 23:39:27 +05:30
Laurent Cozic
43bab3c1bd Merge branch 'master' of github.com:laurent22/joplin 2018-05-16 15:34:09 +01:00
Laurent Cozic
dd67602b87 ios v10.0.22 2018-05-16 15:33:52 +01:00
Laurent Cozic
c96a416c2c Clipper: Clean up 2018-05-16 14:21:33 +01:00
Laurent Cozic
c4ca9cde32 Electron: Allow copying path of resources 2018-05-16 14:16:43 +01:00
Laurent Cozic
6c5d208893 Clipper: Added first files 2018-05-16 14:16:14 +01:00
Laurent Cozic
795fd8b58c HtmlToMD: Finish restoring codes that could be saved, and scoped tests to ENEX-like data 2018-05-16 10:56:30 +01:00
Laurent Cozic
c226940792 HtmlToMd: Re-applied recent changes that can be kept 2018-05-16 10:09:46 +01:00
Laurent Cozic
bdd0a6106f HtmlToMd: Revert to version a677b2e844 2018-05-16 10:09:07 +01:00
Laurent Cozic
d1ea7ad3ea HtmlToMd: Minor fix on table 2018-05-16 09:54:13 +01:00
Laurent Cozic
a2b1181f7c HtmlToMd: Fixed handling of inline tags 2018-05-15 13:26:53 +01:00
Laurent Cozic
8cce2f17d5 HtmlToMd: Improved import of inline and code block 2018-05-15 13:13:55 +01:00
Laurent Cozic
658b911513 HtmlToMd: Minor tweak on LI tag 2018-05-15 09:52:56 +01:00
Laurent Cozic
3c95979d94 HtmlToMd: Fix LI tags that contains blocks 2018-05-14 23:13:17 +01:00
Laurent Cozic
2e32211a28 HtmlToMd: Fix conversion of tables 2018-05-14 23:04:40 +01:00
Laurent Cozic
ba2874173d HtmlToMd: Added more test cases and fixed some issues 2018-05-14 19:48:52 +01:00
Laurent Cozic
ba9598682c HtmlToMd: Fixed various tests 2018-05-14 18:46:04 +01:00
Laurent Cozic
30bfd82683 Update website 2018-05-14 12:47:51 +01:00
Laurent Cozic
10c6774c28 Fix download links 2018-05-14 12:47:36 +01:00
Laurent Cozic
c4ad9019aa Update website 2018-05-14 12:42:06 +01:00
Laurent Cozic
7c99ab9947 Update Readme for portable app 2018-05-14 12:41:45 +01:00
Laurent Cozic
feb7778fe4 Electron release v1.0.93 2018-05-14 12:18:40 +01:00
Laurent Cozic
b45185780f Fixed auto-update download url 2018-05-14 12:18:00 +01:00
Laurent Cozic
4e032c0c55 Electron release v1.0.92 2018-05-14 11:36:40 +01:00
Laurent Cozic
2e2b35dfeb Fixed import of tables and added tests 2018-05-14 11:36:02 +01:00
Laurent Cozic
526ef7e1d2 Don't add new table when processing a table 2018-05-14 11:24:19 +01:00
Laurent Cozic
a37005446a Fixed tests 2018-05-14 11:23:18 +01:00
Laurent Cozic
e012b927dc Merge branch 'clipper' 2018-05-14 11:09:57 +01:00
Laurent Cozic
359b8d5545 Various tweaks to build portable version 2018-05-14 11:08:33 +01:00
Laurent Cozic
23c592b322 Merge branch 'master' of github.com:laurent22/joplin 2018-05-14 10:31:32 +01:00
Laurent Cozic
9aeddf86f4 Updated PortableApps build files 2018-05-14 10:31:16 +01:00
FoxMaSk
0e1887988e add OneDrive
add OneDrive to the general description of the supported services
2018-05-13 14:42:36 +02:00
Laurent Cozic
394f2df664 All: More robust HTML to MD conversion and started adding test units for it 2018-05-12 11:48:39 +01:00
Laurent Cozic
2a04378a0d Merge branch 'clipper' 2018-05-11 15:32:45 +01:00
Laurent Cozic
bac68f2c42 All: Various changes to improve import of HTML 2018-05-11 15:29:44 +01:00
Laurent Cozic
0f0ff86ffa Merge pull request #409 from petrz12/better-evernote-import
Merge monospace text lines when importing from Evernote
2018-05-11 15:25:26 +01:00
Laurent Cozic
8b38752cbf Fix downloads 2018-05-11 14:36:52 +01:00
Laurent Cozic
3c24589450 Fixed deployment script 2018-05-11 14:36:22 +01:00
Laurent Cozic
65065a62d8 CLI v1.0.107 2018-05-11 13:50:19 +01:00
Laurent Cozic
482e9340bc Android release v1.0.125 2018-05-10 21:31:58 +01:00
Laurent Cozic
69d490996e Mobile: Remove uneeded GCM and C2DM dependencies from Android to make it acceptable for F-Droid 2018-05-10 21:25:06 +01:00
Laurent Cozic
3494937e34 Mobile: Resolves #503: Share note with other apps 2018-05-10 20:39:41 +01:00
Laurent Cozic
41ba1043be All: Fixed incorrect timeout for sync-after-save (was using ms instead of sec). Removed needless caching of note IDs in database. 2018-05-10 19:50:44 +01:00
Laurent Cozic
cc57de60c0 Update website 2018-05-10 15:48:16 +01:00
Laurent Cozic
60a2b9e5c6 Electron release v1.0.91 2018-05-10 15:24:46 +01:00
Laurent Cozic
8e1fb666a5 Electron: Fixes #510: Removed reference to missing file 2018-05-10 15:24:38 +01:00
Laurent Cozic
f4ad777bbf Update website 2018-05-10 14:32:33 +01:00
Laurent Cozic
2eacf6146a Android release v1.0.124 2018-05-10 12:24:36 +01:00
Laurent Cozic
fe2ba34cb4 Electron release v1.0.90 2018-05-10 12:22:33 +01:00
Laurent Cozic
84daa0db61 Update readme 2018-05-10 12:22:14 +01:00
Laurent Cozic
b9118a90be All: Resolves #443: Various optimisations to make dealing with large notes easier and make Katex re-rendering faster 2018-05-10 12:02:39 +01:00
Laurent Cozic
ef2ffd4e52 Electron: Resolves #200, Resolves #416: Allow attaching images by pasting them in. Allow attaching files by drag and dropping them. Insert attachement at cursor position. 2018-05-10 10:45:44 +01:00
Laurent Cozic
5e3063abe0 Updated translations 2018-05-09 21:05:52 +01:00
Laurent Cozic
f460b2497a Merge pull request #506 from fmrtn/master
Updated Spanish translation
2018-05-09 21:04:20 +01:00
Laurent Cozic
c080d7054f Merge branch 'master' of github.com:laurent22/joplin 2018-05-09 21:00:33 +01:00
Laurent Cozic
61dd4cefbc All: Resolves #345: Option to hide completed todos 2018-05-09 21:00:05 +01:00
Laurent Cozic
63d99b2d70 Mobile: Fixes #497: Make sure Dropbox text input is visible when keyboard is visible on iPhone SE 2018-05-09 19:11:48 +01:00
Laurent Cozic
55332d7671 Electron: Fixes #481: Shortcuts were not working when text editor had focus 2018-05-09 18:41:32 +01:00
Laurent Cozic
16635defcd Mobile: Fixes #455: Use active folder when creating new note from Welcome screen 2018-05-09 18:12:00 +01:00
Laurent Cozic
595cf3fcad Mobile: Fixes #433: Don't scroll note back to top when changing checkbox state 2018-05-09 18:04:48 +01:00
Fernando
c9b9f82130 Updated Spanish translation 2018-05-09 18:48:32 +02:00
Laurent Cozic
f5bca733d7 Fixed translator email encoding issue 2018-05-09 17:06:02 +01:00
Laurent Cozic
494e235e18 Electron: Resolves #500: Fixed XSS security vulnerability 2018-05-09 16:59:33 +01:00
Laurent Cozic
85219a6004 Android release v1.0.123 2018-05-09 16:43:33 +01:00
Laurent Cozic
e4a7851e57 Update debugging.md 2018-05-09 16:33:16 +01:00
Laurent Cozic
b7529b40b5 Updated tests 2018-05-09 16:14:27 +01:00
Laurent Cozic
74827e5324 Electron: Fixed tag display 2018-05-09 15:31:42 +01:00
Laurent Cozic
2e16cc5433 ios-v10.0.21 2018-05-09 14:15:04 +01:00
Laurent Cozic
7f41bc5703 Update website 2018-05-09 14:10:13 +01:00
Laurent Cozic
a2380fb752 Android release v1.0.122 2018-05-09 13:18:39 +01:00
Laurent Cozic
f6a902809d Electron release v1.0.89 2018-05-09 13:17:08 +01:00
Laurent Cozic
33a853397d Electron release v1.0.88 2018-05-09 13:16:55 +01:00
Laurent Cozic
4f02481899 Electron release v1.0.87 2018-05-09 13:14:42 +01:00
Laurent Cozic
b18076565f Update translations 2018-05-09 13:14:17 +01:00
Laurent Cozic
853ddc5840 Update website 2018-05-09 13:11:03 +01:00
Laurent Cozic
7930ab66c6 Merge branch 'master' into subnotebooks 2018-05-09 13:10:20 +01:00
Laurent Cozic
c7716c0d59 All: Resolves #122: Sub-notebook support in desktop, mobile and cli app 2018-05-09 13:08:00 +01:00
Laurent Cozic
49cbb254d0 CLI: Fixed link handling 2018-05-09 12:50:50 +01:00
Laurent Cozic
cf9246796d CLI: Added support for sub-notebooks 2018-05-09 12:39:27 +01:00
Laurent Cozic
e1dee546dc Mobile: Added support for sub-notebooks 2018-05-09 12:39:17 +01:00
Laurent Cozic
da6fdad2de All: Handle saving collapsed states of sub-notebook 2018-05-09 10:49:31 +01:00
Laurent Cozic
567596643c Electron: Handle drag and dropping notebooks to change the parent 2018-05-09 09:53:47 +01:00
Laurent Cozic
cb617e1b14 All: Fixes #61: Handle path that ends with slash for file system sync. 2018-05-08 11:29:25 +01:00
Laurent Cozic
facf8afa8b Update translations 2018-05-08 11:12:36 +01:00
Laurent Cozic
f0dd61a711 Merge pull request #495 from fmrtn/master
Updated Spanish translation
2018-05-08 11:12:01 +01:00
Laurent Cozic
e958211a13 Merge pull request #496 from zuphilip/patch-1
Update address pronouns "du" in German translation
2018-05-08 11:11:45 +01:00
Philipp Zumstein
0ed170b5bc Update address pronouns "du" in German translation 2018-05-07 07:12:01 +02:00
Fernando
473d3453a2 Updated Spanish translation 2018-05-06 20:29:35 +02:00
Laurent Cozic
fa9d7b0408 Electron: Started UI and backend for sub-notebook support 2018-05-06 12:11:59 +01:00
Laurent Cozic
d4a28f48c9 Update website 2018-05-06 11:17:34 +01:00
Laurent Cozic
ead6fff861 Merge branch 'master' of github.com:laurent22/joplin 2018-05-06 11:16:52 +01:00
Laurent Cozic
c7d06b35cd Merge pull request #494 from stweil/typo
Fix some typos
2018-05-06 11:16:43 +01:00
Laurent Cozic
fa939e5c76 Merge branch 'master' of github.com:laurent22/joplin 2018-05-06 11:16:19 +01:00
Laurent Cozic
1bf2601f4f Merge pull request #492 from zuphilip/patch-1
Update de_DE.po
2018-05-06 11:15:59 +01:00
Stefan Weil
feb0c02c9a ReactNativeClient: Fix some typos (found by codespell)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:25:37 +02:00
Stefan Weil
40a34a7c05 Fix typos in documentation (found by codespell)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:23:48 +02:00
Stefan Weil
c62dcd96b0 CliClient: Fix some typos (found by codespell)
Remove also a "translation" which was none from locales/hr_HR.po.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:22:14 +02:00
Philipp Zumstein
1364d6786d Update de_DE.po 2018-05-05 11:08:47 +02:00
petrz12
9f2666aef9 Evernote code block conversion added, better handling of monospace empty lines 2018-05-04 00:23:19 +02:00
Laurent Cozic
a6a351e68d Electron: Export/Import links to notes 2018-05-03 13:11:45 +01:00
Laurent Cozic
1db38a9699 Merge pull request #484 from fmrtn/master
Spanish translation updated
2018-05-03 12:08:57 +01:00
Fernando
c57db1834f Spanish translation updated 2018-05-03 13:07:36 +02:00
Laurent Cozic
3aeb49b469 Merge branch 'master' of github.com:laurent22/joplin 2018-05-03 11:31:17 +01:00
Laurent Cozic
80b467eead All: For now, disable attaching resources larger than 10MB due to #371 2018-05-03 11:31:07 +01:00
Laurent Cozic
61572f287a Update README.md
Added note about large resources
2018-05-03 11:06:12 +01:00
petrz12
f136664c11 codeblock detection added 2018-05-02 22:39:50 +02:00
Laurent Cozic
0e545baf10 Update issue_template.md 2018-05-02 21:39:11 +01:00
Laurent Cozic
e65e647359 Update issue_template.md 2018-05-02 21:38:49 +01:00
Laurent Cozic
238268884e Update debugging.md 2018-05-02 20:16:08 +01:00
Laurent Cozic
4c210d0956 Electron: Fixes #480: Ignore invalid flag automatically passed by macOS 2018-05-02 15:51:33 +01:00
Laurent Cozic
5f32c6466a Update website 2018-05-02 15:33:18 +01:00
Laurent Cozic
71bd39a8a3 Update website 2018-05-02 15:31:17 +01:00
Laurent Cozic
ffb660f0f4 Move tool file 2018-05-02 15:28:13 +01:00
Laurent Cozic
dde23632c1 Move tool file 2018-05-02 15:27:12 +01:00
Laurent Cozic
9d26f13db0 Android release v1.0.120 2018-05-02 15:18:13 +01:00
Laurent Cozic
2a4c9c4427 Electron release v1.0.86 2018-05-02 15:16:02 +01:00
Laurent Cozic
3bfde26b74 Merge branch 'master' of github.com:laurent22/joplin 2018-05-02 15:13:28 +01:00
Laurent Cozic
a419bc7253 All: Resolves #134: Allow linking to a note from another note 2018-05-02 15:13:20 +01:00
Laurent Cozic
89e0dad88b Update README.md
32-bit is now supported
2018-05-02 13:16:04 +01:00
Laurent Cozic
ff1ee1249b Mobile: Resolves #61: Enable File System sync on mobile as the driver seems to be working now 2018-05-02 10:27:37 +01:00
Laurent Cozic
ba9cfd8041 Electron: Increased timeout for sync-after-save to 30 seconds 2018-05-02 08:34:54 +01:00
Laurent Cozic
80a51e02a4 Update readme downloads 2018-05-01 22:33:01 +01:00
Laurent Cozic
a2e2a9a2f5 Electron release v1.0.85 2018-05-01 21:16:24 +01:00
Laurent Cozic
49e4c37cac Electron: Check that the filename contains 'Setup' when auto-updating 2018-05-01 21:13:41 +01:00
Laurent Cozic
11d323d8b7 Electron: Fixes #479: Currently loaded note was cleared when creating new note 2018-05-01 21:13:17 +01:00
Laurent Cozic
784ba45f1f Electron release v1.0.84 2018-05-01 19:39:06 +01:00
Laurent Cozic
e534414874 All: Fixes #434: Handle Katex block mode 2018-05-01 19:34:42 +01:00
Laurent Cozic
01f4faf8f1 Mobile: Fixes #426: Fix missing menu icon on Android 4 2018-05-01 19:30:41 +01:00
Laurent Cozic
b33d30ca47 Merge branch 'master' of github.com:laurent22/joplin 2018-05-01 19:05:33 +01:00
Laurent Cozic
1ba3fae101 All: Resolves #470: Make it clear that spaces in URLs are invalid. 2018-05-01 19:05:14 +01:00
Laurent Cozic
9550347e04 Merge pull request #448 from solariz/master
Fix for Issue #430
2018-05-01 18:54:43 +01:00
Laurent Cozic
398946d39a Mobile: Fixes #393: Fixed moving new notes before they are saved 2018-05-01 18:53:45 +01:00
Laurent Cozic
05faf55e8d All: Fixes #363: Fixed indentation and rendering of lists 2018-05-01 16:45:17 +01:00
Laurent Cozic
4cf5525e20 Electron: Fixes #355: Set undo state properly when loading new note 2018-05-01 10:48:15 +01:00
Laurent Cozic
62e91c44d7 Electron: Fixes #346: Make sure links have an address when exporting to PDF 2018-05-01 10:14:48 +01:00
Laurent Cozic
e4ec4ae92b Mobile: Fix action button when note is being edited. 2018-05-01 10:09:36 +01:00
Laurent Cozic
c1f5dfd9cc Keep Blob tests to revisit in a few weeks 2018-04-30 21:21:17 +01:00
Laurent Cozic
0c0efeac1f Android release v1.0.119 2018-04-30 18:34:55 +01:00
Laurent Cozic
5e0f2642e3 Android release v1.0.118 2018-04-30 17:41:39 +01:00
Laurent Cozic
93966b0fa1 Mobile: Trying to upgrade to React Native 0.55 2018-04-30 17:38:19 +01:00
Laurent Cozic
e90abf3517 Updated translations 2018-04-28 11:44:07 +02:00
Laurent Cozic
d3fa0dce96 Updated translations 2018-04-28 11:40:55 +02:00
Laurent Cozic
58a7c2fa94 Doc: Added forum link to website 2018-04-28 11:40:27 +02:00
Laurent Cozic
962a8700c2 Added ptBR translation 2018-04-28 11:35:28 +02:00
Laurent Cozic
b5c704e2bb Doc: Updated website 2018-04-25 20:08:59 +02:00
Laurent Cozic
e7b52b19d7 Doc: Updated website 2018-04-25 20:04:25 +02:00
Laurent Cozic
903c2e6d92 Doc: Updated website 2018-04-25 19:25:54 +02:00
Laurent Cozic
abcb1ac760 Doc: Update website 2018-04-25 19:24:13 +02:00
Laurent Cozic
b6bf76cc4c Doc: Mentioned forums and other community-related links 2018-04-25 11:59:43 +02:00
Laurent Cozic
2bf87655da Electron: Various tweaks to get PortableApps format to work 2018-04-23 21:50:29 +02:00
Laurent Cozic
d4b19f19a1 Electron: Use built-in image resizing instead of Sharp 2018-04-22 21:10:43 +02:00
Laurent Cozic
d8ccc38d5b All: Better handling of resources that are incorrectly flagged as encrypted 2018-04-22 14:33:12 +02:00
petrz12
c8c9f80cc5 Making monospace merging permanent 2018-04-21 13:55:18 +02:00
Marco G
577bef5704 Fix for Issue #430 2018-04-21 10:25:13 +02:00
Laurent Cozic
4e3b8a06ea Merge branch 'master' of github.com:laurent22/joplin 2018-04-19 19:43:27 +02:00
Laurent Cozic
363632ffa7 Doc: Added Arch Linux package info 2018-04-19 19:42:44 +02:00
Laurent Cozic
994c99f47f Merge pull request #442 from solariz/master
Common EU Date format DD.MM.YYYY added, issue #405
2018-04-19 19:32:22 +02:00
Marco G
96571baadc Common EU Date format DD.MM.YYYY added, issue #405 2018-04-19 17:03:16 +02:00
Laurent Cozic
4ce2b2c948 Doc: Added link to AUR package 2018-04-19 11:14:43 +02:00
Laurent Cozic
5d69f7a0a7 Doc: Added link to AUR package 2018-04-19 11:13:33 +02:00
petrz12
69ddcc6e30 Remove debug lines, fixing some comments 2018-04-18 14:13:19 +02:00
Laurent Cozic
bcb1f36ad8 Merge pull request #428 from Abijeet/master
Add the toggle sidebar option to the View app menu bar.
2018-04-17 11:17:24 +02:00
Abijeet
34c65a686c Add the toggle sidebar option to the View app menu bar.
Towards #183

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-16 22:57:36 +05:30
Laurent Cozic
0b32741a12 Electron: Resolves #252: Allow enabling tray icon in Linux 2018-04-16 19:18:28 +02:00
Laurent Cozic
dbb321a3cc CLI: Resolves #411: Fixed documentation 2018-04-16 16:29:34 +02:00
Laurent Cozic
a6e4f47adf All: Resolves #389: Documented method being used to encrypt master keys 2018-04-16 16:16:31 +02:00
Laurent Cozic
fb6dee32ac All: Resolves #380: Fixed documentation inconsitencies for webdav url 2018-04-16 16:09:38 +02:00
Laurent Cozic
984dd6f2c0 Electron: Rotate sidebar icon when it is hidden 2018-04-16 15:32:33 +02:00
Laurent Cozic
02bde2c6e9 Merge pull request #424 from Abijeet/master
Adds support to toggle the sidebar.
2018-04-16 15:24:33 +02:00
Laurent Cozic
782d24cc04 Updated Spanish translation 2018-04-16 15:16:29 +02:00
Laurent Cozic
4d0af575e5 Mobile: Resolves #360: Don't crash if theme not set and improved logging. 2018-04-16 15:15:29 +02:00
Laurent Cozic
be8bda8e73 Added Joplin letter as SVG 2018-04-16 11:20:33 +02:00
Abijeet
1242de532e Adds support to toggle the sidebar.
Closes #183

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-15 21:20:39 +05:30
Laurent Cozic
7d7ec7f15e Update translation 2018-04-11 21:39:37 +02:00
Laurent Cozic
ca112ec5d3 Merge branch 'master' of github.com:laurent22/joplin 2018-04-11 21:39:13 +02:00
Laurent Cozic
5deb8cf76d Update translation 2018-04-11 19:27:45 +02:00
Laurent Cozic
a2c9737c17 utf-8 2018-04-11 19:27:20 +02:00
Laurent Cozic
d3fca3d6cc Mobile: Fixes #358: Fix cursor color in dark mode 2018-04-11 19:25:07 +02:00
petrz12
16554b22c7 Merge monospace text lines when importing from Evernote 2018-04-10 22:59:45 +02:00
Laurent Cozic
d5574098f0 Resolves #392: Clarified deletion of resources 2018-04-09 21:00:47 +02:00
Laurent Cozic
f5a683f25c Update issue_template.md 2018-04-09 20:58:35 +02:00
Laurent Cozic
5f04adb392 Merge pull request #376 from mjjzf/master
Updated Danish translation
2018-04-05 22:41:21 +02:00
Laurent Cozic
edd0f7e255 Update issue_template.md 2018-04-04 21:50:45 +02:00
Laurent Cozic
67145d9104 Updated website 2018-04-04 21:48:42 +02:00
Laurent Cozic
003e2afff7 Update readme downloads 2018-04-04 21:44:25 +02:00
Laurent Cozic
6e9d70c5cb Electron release v1.0.83 2018-04-04 21:25:45 +02:00
Laurent Cozic
4821b4cdf2 Electron: Fixes #365: Cannot paste in Dropbox screen 2018-04-04 21:25:27 +02:00
Morten Juhl-Johansen Zölde-Fejer
734d4db431 Updated Danish translation
Translated new strings, minor corrections
2018-04-04 12:32:30 +02:00
Morten Juhl-Johansen Zölde-Fejer
317aaed0ac Updated Danish translation
New strings and minor corrections.
2018-04-04 11:52:26 +02:00
Laurent Cozic
9778098d6c Merge pull request #366 from bimlas/patch-1
Clarify Windows build dependencies
2018-04-02 21:17:10 +02:00
Bimba Laszlo
5b1755f988 Clarify Windows build dependencies 2018-04-02 19:14:04 +02:00
Laurent Cozic
2a772895dd ios v10.0.20 2018-03-31 20:24:32 +02:00
Laurent Cozic
5fbb01cf2f Electron release v1.0.82 2018-03-31 19:23:06 +01:00
Laurent Cozic
f9e0870b4e Android release v1.0.116 2018-03-31 19:21:07 +01:00
Laurent Cozic
a58f1e9b4b Update Galician translation 2018-03-31 19:19:11 +01:00
Laurent Cozic
6fc0d89b30 Update website 2018-03-29 20:55:15 +01:00
Laurent Cozic
2dcadab7d2 Update website 2018-03-29 20:50:18 +01:00
Laurent Cozic
bb3307e156 Update website 2018-03-29 20:48:22 +01:00
Laurent Cozic
ecd07f1209 Update website 2018-03-29 20:42:11 +01:00
Laurent Cozic
266cb1174f Update website 2018-03-29 20:36:34 +01:00
Laurent Cozic
bfb9b77b6e Update website 2018-03-29 20:25:37 +01:00
Laurent Cozic
01b1361dcb Update website 2018-03-29 20:21:14 +01:00
Laurent Cozic
3a921720d6 Update website 2018-03-29 20:17:11 +01:00
Laurent Cozic
cdfd3d9c31 Updated French translation and added Galician translation 2018-03-29 18:30:33 +01:00
Laurent Cozic
9961fb64bb Updated website 2018-03-29 00:52:27 +01:00
Laurent Cozic
3137c355cf Added stats and changelog document 2018-03-28 17:41:14 +00:00
Laurent Cozic
16abaf60d2 Update readme downloads 2018-03-28 09:14:24 +01:00
Laurent Cozic
9004b710ea CLI v1.0.106 2018-03-27 23:22:06 +01:00
Laurent Cozic
6ebac21c2b Electron release v1.0.81 2018-03-27 23:03:25 +01:00
Laurent Cozic
99f79faf83 Android release v1.0.115 2018-03-27 23:01:56 +01:00
Laurent Cozic
613fa20806 Merge branch 'dropbox' 2018-03-27 23:00:49 +01:00
Laurent Cozic
1b5f812278 Merge branch 'master' of github.com:laurent22/joplin 2018-03-27 23:00:32 +01:00
Laurent Cozic
3a9643c1ea Electron release v1.0.80 2018-03-27 22:57:23 +01:00
Laurent Cozic
aee7f5a8ac Android release v1.0.114 2018-03-27 22:55:46 +01:00
Laurent Cozic
d3cd378922 Android release v1.0.113 2018-03-27 17:57:34 +01:00
Laurent Cozic
4f5e7367d0 Minor tweals 2018-03-27 17:48:55 +01:00
Laurent Cozic
2280fb5c43 Styled Dropbox mobile GUI 2018-03-27 17:41:19 +00:00
Laurent Cozic
96fb7c2087 Getting Dropbox to work in mobile app 2018-03-27 00:55:44 +01:00
Laurent Cozic
6e994fd8b9 All: Dropbox: Handle various error conditions 2018-03-27 00:05:39 +01:00
Laurent Cozic
a7cde1e269 All: Resolves #329: Add link to E2EE doc 2018-03-26 17:55:09 +00:00
Laurent Cozic
f8310ba0d5 Mobile: Fixed tag box styling in dark theme 2018-03-26 17:52:49 +00:00
Laurent Cozic
b239c3faba Update README.md 2018-03-26 18:51:03 +01:00
Laurent Cozic
3c2281dbf9 Merge branch 'master' into dropbox 2018-03-26 18:37:04 +01:00
Laurent Cozic
ac07bf784d Adding Dropbox sync to Electron app 2018-03-26 18:33:55 +01:00
Laurent Cozic
067455542f Update README.md 2018-03-25 20:02:54 +01:00
Laurent Cozic
5bfeaa357b Updated translations and added Czech translation 2018-03-24 20:15:36 +00:00
Laurent Cozic
fe27a64331 Merge branch 'master' of github.com:laurent22/joplin 2018-03-24 20:14:04 +00:00
Laurent Cozic
ed638612aa Moved to https:// domain 2018-03-24 20:13:52 +00:00
Laurent Cozic
1d7ec83510 Merge pull request #328 from hydrandt/czech_translation
adding Czech translation
2018-03-24 20:12:44 +00:00
Laurent Cozic
75c710232d Update readme downloads 2018-03-24 20:08:23 +00:00
hydrandt
5af52afadb adding Czech translation 2018-03-25 04:01:51 +08:00
Laurent Cozic
0f4324c2f8 All: Added backend for Dropbox support 2018-03-24 19:35:10 +00:00
Laurent Cozic
b48e1dac94 Update README.md
Fixes #327
2018-03-24 16:50:10 +00:00
Laurent Cozic
f0ca8e1e31 Update debugging.md 2018-03-24 15:37:33 +00:00
Laurent Cozic
74b83eb71e All: Fixes #318: Display full links in editor 2018-03-24 15:16:58 +00:00
Laurent Cozic
28dce0fbb5 iOS 19 2018-03-23 19:19:15 +00:00
Laurent Cozic
c12d402c7e Merge branch 'master' of github.com:laurent22/joplin 2018-03-23 18:19:46 +00:00
Laurent Cozic
014f5b123c iOS 15 2018-03-23 18:19:38 +00:00
Laurent Cozic
58601dfc04 CLI v1.0.104 2018-03-23 18:06:06 +00:00
Laurent Cozic
9fe7f0adae Android release v1.0.112 2018-03-23 17:32:29 +00:00
Laurent Cozic
ea1374371f Electron release v1.0.79 2018-03-23 17:30:24 +00:00
Laurent Cozic
bce4294529 Fix Mark loading state logic 2018-03-23 17:29:08 +00:00
Laurent Cozic
de409b632a Merge branch 'master' into highlight-search 2018-03-23 17:25:20 +00:00
Laurent Cozic
a677b2e844 Electron: Fixes #318, Fixes #317: ENEX: Improved handling and rendering of plain text links. Improved detection and import of resources. Improved import of tables. 2018-03-23 17:59:18 +00:00
Laurent Cozic
c63bb19cb6 Updated French translation and added Danish translation 2018-03-22 18:25:50 +00:00
Laurent Cozic
72fd77812e Update README.md 2018-03-21 07:48:29 +00:00
Laurent Cozic
40f3e72bd1 Update website 2018-03-20 17:41:48 +00:00
Laurent Cozic
d6d86f2aff Merge branch 'master' of github.com:laurent22/joplin 2018-03-20 17:37:36 +00:00
Laurent Cozic
c71809438b Updated website CSS 2018-03-20 17:36:07 +00:00
Laurent Cozic
3e6e1a0a36 Added badges 2018-03-20 10:24:26 +00:00
Laurent Cozic
f590ce4a34 Added Nextcloud logo and intro text 2018-03-19 23:25:55 +00:00
Laurent Cozic
67608e29c8 Electron: Resolves #144, Resolves #311: Highlight search results and search in real time. Associated Ctrl+F with searching. 2018-03-19 23:04:48 +00:00
Laurent Cozic
d5c2982093 Merge branch 'master' of github.com:laurent22/joplin 2018-03-18 12:45:59 +00:00
Laurent Cozic
90fad2a3ab Electron: Resolves #307: Use blue colour for sidebar, to be consistent with mobile app and logo 2018-03-18 12:45:39 +00:00
Laurent Cozic
bc7c82e3da Update README.md 2018-03-18 10:16:08 +00:00
Laurent Cozic
cb824f7dd7 Android release v1.0.110 2018-03-17 23:55:09 +00:00
Laurent Cozic
32c47a96f1 Electron: Resolves #73: Show modified date next to note in editor 2018-03-17 23:51:15 +00:00
Laurent Cozic
4e3f8893f7 Merge branch 'master' of github.com:laurent22/joplin 2018-03-17 23:00:22 +00:00
Laurent Cozic
ca3946689a Mobile: Made tag UI a dialog 2018-03-17 23:00:01 +00:00
Laurent Cozic
e2ad2dfcaa Update CONTRIBUTING.md 2018-03-17 22:23:50 +00:00
Laurent Cozic
d6f7893c56 Update readme downloads 2018-03-17 15:28:56 +00:00
Laurent Cozic
8c65a7cc31 Electron release v1.0.78 2018-03-17 15:13:00 +00:00
Laurent Cozic
aabb9be7de Mobile: Resolves #285: Create, edit and remove tags from notes 2018-03-16 20:17:52 +00:00
Laurent Cozic
544f93bf22 All: Handle deletion of resources that are not linked to any note 2018-03-16 17:39:44 +00:00
Laurent Cozic
f81dbf4a4c CLI v1.0.103 2018-03-16 14:36:09 +00:00
Laurent Cozic
fbec8263a3 Electron release v1.0.77 2018-03-16 14:34:10 +00:00
Laurent Cozic
68d77a69e6 Updated translations 2018-03-16 14:33:53 +00:00
Laurent Cozic
f2ef2446c6 Merge branch 'master' of github.com:laurent22/joplin 2018-03-16 14:33:16 +00:00
Laurent Cozic
875cb5387a Merge pull request #306 from tobias-grasse/tobias-grasse-patch-1
Update de_DE.po
2018-03-16 14:33:02 +00:00
Laurent Cozic
ae9ecdad40 All: Fix database upgrade 2018-03-16 14:32:47 +00:00
Laurent Cozic
86a0e34975 Update faq.md 2018-03-16 14:19:24 +00:00
Laurent Cozic
1141074745 Update faq.md 2018-03-16 14:19:10 +00:00
Tobias Grasse
efc46d9989 Update de_DE.po
Add missing translations, minor corrections for existing translations.
2018-03-16 12:13:42 +01:00
Laurent Cozic
2b45f745b6 Electron release v1.0.76 2018-03-16 10:12:27 +00:00
Laurent Cozic
37fb81e9b2 Trying to build only master branch 2018-03-16 10:12:23 +00:00
Laurent Cozic
255a4fac93 Electron release v1.0.75 2018-03-16 10:03:47 +00:00
Laurent Cozic
3e3fb88de8 Merge branch 'master' into resource_cleanup 2018-03-16 10:03:05 +00:00
Laurent Cozic
e4cf03ae46 Only build master branch 2018-03-16 10:02:58 +00:00
Laurent Cozic
554a3eb10d Electron release v1.0.74 2018-03-16 09:11:25 +00:00
Laurent Cozic
61881b528a Electron: Trying to fix signed executable issue 2018-03-16 09:11:10 +00:00
Laurent Cozic
c2507cbc4e CLI v1.0.101 2018-03-16 08:17:27 +00:00
Laurent Cozic
ed0f6d165c Android release v1.0.107 2018-03-16 08:10:32 +00:00
Laurent Cozic
8e22d38eb3 Electron release v1.0.73 2018-03-16 08:08:10 +00:00
Laurent Cozic
2599c425c3 Updated translations 2018-03-15 18:15:33 +00:00
Laurent Cozic
0e15821a81 Merge branch 'master' of github.com:laurent22/joplin 2018-03-15 18:10:11 +00:00
Laurent Cozic
c1bb51c12b All: Finished service to clean up resources 2018-03-15 18:08:46 +00:00
Laurent Cozic
1532b6d159 All: Made WebDAV options dynamics so that changing username or password doesn't require restarting the app 2018-03-15 17:57:11 +00:00
Laurent Cozic
945018b698 All: Allow deleting and syncing deleted resources 2018-03-15 17:46:54 +00:00
Laurent Cozic
df7b981e5e Merge branch 'master' into resource_cleanup 2018-03-15 18:19:06 +00:00
Laurent Cozic
4fe495675b Merge pull request #301 from fmrtn/master
Updated Spanish translation
2018-03-15 14:26:28 +00:00
Fernando
7828eef2ad Updated Spanish translation 2018-03-15 15:24:37 +01:00
Laurent Cozic
694f81b75f Merge pull request #300 from rtmkrlv/master
Updated Russian translation
2018-03-15 13:15:09 +00:00
rtmkrlv
8364b6e08d Updated Russian translation 2018-03-15 11:14:04 +02:00
rtmkrlv
3f4328ce9d Merge pull request #5 from laurent22/master
Update fork from original repository
2018-03-15 10:41:44 +02:00
Laurent Cozic
9e0bf1acb2 Mobile: Fixes #299: App freezes when connecting to OneDrive login page while internet connection is down 2018-03-14 23:17:02 +00:00
Laurent Cozic
c9e130a771 Merge branch 'master' into resource_cleanup 2018-03-14 17:41:06 +00:00
Laurent Cozic
26331f61e1 Electron: Resolves #298: Removed extraneous first characters from auto-title 2018-03-14 17:37:47 +00:00
Laurent Cozic
694672859a Updated translations 2018-03-14 17:32:05 +00:00
Laurent Cozic
858ead40b9 /bin/bash: qa: command not found 2018-03-14 17:29:13 +00:00
Laurent Cozic
b07fe5cc34 CLI: Fixes #288: Don't log commands in release versions 2018-03-14 17:28:41 +00:00
Laurent Cozic
0317171097 Electron: Fixes #292: Removed buggy electron-updater and implemented custom check 2018-03-14 17:23:19 +00:00
Laurent Cozic
9741a3a53d Merge pull request #283 from jcgerhard/master
Updated german translation de_DE
2018-03-14 16:44:54 +00:00
Laurent Cozic
7937fab5ff Electron: Fixes #291: Crash with empty backtick 2018-03-13 17:54:40 +00:00
Laurent Cozic
f595be07d4 Adding service to keep track of note resources associations 2018-03-12 23:40:43 +00:00
Laurent Cozic
eef106c99b Electron: Resolves #237: Export to PDF and print option 2018-03-12 18:01:47 +00:00
Laurent Cozic
dbe1833f92 Made easier to add export options 2018-03-12 08:30:10 +00:00
Jan C. Gerhard
520dc0ae21 Updated german translation de_DE 2018-03-12 08:08:14 +01:00
Laurent Cozic
c9be287f4a Electron release v1.0.72 2018-03-11 19:34:07 +00:00
Laurent Cozic
711f5dcaba Android release v1.0.106 2018-03-11 19:32:41 +00:00
Laurent Cozic
ebc0aa9809 Electron release v1.0.71 2018-03-11 19:31:07 +00:00
Laurent Cozic
dcaaf50a5a All: Fixes #271: Sort by created time was not respected 2018-03-10 15:34:55 +00:00
Laurent Cozic
3370b57134 Finished removing prettier 2018-03-10 15:34:29 +00:00
Laurent Cozic
55c5ddedf4 Revert "Applied prettier to code base"
This reverts commit c4f19465a6.
2018-03-09 20:59:12 +00:00
Laurent Cozic
5e8b09f5af All: Display icon next to resources and allow downloading them from Electron client 2018-03-09 20:46:28 +00:00
Laurent Cozic
1acffce62d All: Display last sync error unless it's a timeout or network error 2018-03-09 19:51:01 +00:00
Laurent Cozic
8555ecce87 Merge branch 'master' of github.com:laurent22/joplin 2018-03-09 19:07:47 +00:00
Laurent Cozic
4df5f668dc All: Improved sync when dealing with many items, in particular when using Nextcloud 2018-03-09 19:07:38 +00:00
Laurent Cozic
cceebeebef Update CONTRIBUTING.md 2018-03-09 17:59:41 +00:00
Laurent Cozic
c4f19465a6 Applied prettier to code base 2018-03-09 17:49:35 +00:00
Laurent Cozic
e868102c98 Revert "Adding prettier to all projects"
This reverts commit d6a4436313.
2018-03-09 17:45:42 +00:00
Laurent Cozic
0d4a1837f5 Merge branch 'master' of github.com:laurent22/joplin 2018-03-09 17:32:26 +00:00
Laurent Cozic
d6a4436313 Adding prettier to all projects 2018-03-09 17:32:10 +00:00
Laurent Cozic
03b5c6aa5e Added prettier options to ST project 2018-03-09 17:48:08 +00:00
Laurent Cozic
250cd47e02 Added prettier config file 2018-03-09 17:41:34 +00:00
Laurent Cozic
943fef32e7 Merge pull request #281 from fmrtn/master
Updated Spanish translation
2018-03-09 16:59:55 +00:00
Fernando
408634671c Updated Spanish translation 2018-03-09 17:28:39 +01:00
Laurent Cozic
570b5856ba Updated translations 2018-03-09 09:11:13 +00:00
Laurent Cozic
d114d14e87 Added donation links 2018-03-09 09:09:13 +00:00
Laurent Cozic
32791f502e More consistent links 2018-03-09 08:57:34 +00:00
Laurent Cozic
083ab0c788 Updated website 2018-03-09 08:53:26 +00:00
Laurent Cozic
003c4c4e26 Moved readme files to own folder 2018-03-09 08:47:21 +00:00
Laurent Cozic
f08f89ebd4 Added donate page 2018-03-09 08:30:36 +00:00
Laurent Cozic
3c973144c4 Added image 2018-03-09 08:18:02 +00:00
Laurent Cozic
82e99ca658 All: Better handling of startup errors 2018-03-07 19:11:55 +00:00
Laurent Cozic
b04d750cec Merge branch 'master' of github.com:laurent22/joplin 2018-03-07 17:40:00 +00:00
Laurent Cozic
c804e9f541 CLI: Fixes #268: Improve error message for invalid flags 2018-03-07 17:39:45 +00:00
Laurent Cozic
7753f3f842 Minor tweaks 2018-03-07 17:35:11 +00:00
Laurent Cozic
c985b7c682 Merge pull request #277 from fmrtn/master
Updated Spanish translation
2018-03-07 17:30:48 +00:00
Fernando
4509919c22 Updated Spanish translation 2018-03-07 17:04:10 +01:00
Laurent Cozic
89b164c7ca Merge pull request #272 from alexdevero/windows-build-instructions
Add build instructions for Windows
2018-03-06 23:16:32 +00:00
Alex Devero
e52d17b39a Add build instructions for Windows 2018-03-06 08:42:29 +01:00
Laurent Cozic
5014914dc9 Android release v1.0.104 2018-03-05 18:23:11 +00:00
Laurent Cozic
122ab83a84 Android: Fix when downloading many encrypted items 2018-03-05 18:21:42 +00:00
Laurent Cozic
7a985c2c8a Android release v1.0.103 2018-03-02 18:15:09 +00:00
Laurent Cozic
b11ad30a31 Updated translations 2018-03-02 18:12:58 +00:00
Laurent Cozic
5914fc97df Merge branch 'master' of github.com:laurent22/joplin 2018-03-02 18:03:28 +00:00
Laurent Cozic
e41ae1832d Minor tweaks 2018-03-02 18:24:02 +00:00
Laurent Cozic
89b50909ed Electron: Resolves #266: Allow setting text editor font family 2018-03-02 18:16:48 +00:00
Laurent Cozic
edccd7412f Merge pull request #265 from jaredcrowe/docs-correct-checkbox-syntax
correct the documentation about adding checkboxes
2018-03-02 17:59:33 +00:00
Jared Crowe
c76beae057 correct the documentation about adding checkboxes 2018-03-02 09:30:27 +11:00
Laurent Cozic
23c5934a7d Electron: Allow exporting only selected notes or notebook 2018-03-01 20:14:06 +00:00
Laurent Cozic
a078947d6d Allow importing and exporting single notes and notebooks 2018-03-01 18:35:17 +00:00
rtmkrlv
0faaf660b4 Merge pull request #4 from laurent22/master
Update fork from original repository
2018-03-01 15:31:51 +02:00
Laurent Cozic
5ba98b4200 CLI v1.0.100 2018-02-28 21:18:27 +00:00
Laurent Cozic
c36513b99d Updated translation 2018-02-28 20:46:15 +00:00
Laurent Cozic
97814531fa Update readme downloads 2018-02-28 20:06:06 +00:00
Laurent Cozic
fd3e335a02 Electron release v1.0.70 2018-02-28 18:57:10 +00:00
Laurent Cozic
e676fa2b57 Trying to fix Electron builder 2018-02-28 18:56:57 +00:00
Laurent Cozic
122cbbf673 Electron release v1.0.69 2018-02-28 18:43:12 +00:00
Laurent Cozic
271793b324 Trying to upgrade Electron Builder 2018-02-28 18:43:03 +00:00
Laurent Cozic
134b31933b Electron release v1.0.68 2018-02-28 18:13:02 +00:00
Laurent Cozic
0ec5518a62 Android release v1.0.102 2018-02-28 18:10:33 +00:00
Laurent Cozic
76931370d7 Updated readme for import/export 2018-02-27 22:15:20 +00:00
Laurent Cozic
8cf0e4517a Merge branch 'master' of github.com:laurent22/joplin 2018-02-28 17:27:06 +00:00
Laurent Cozic
e75c62bf0f Electron: Fix logic of hidding app in macOS 2018-02-27 21:54:40 +00:00
Laurent Cozic
058285e0b9 All: Made scheduled sync delay slightly shorter 2018-02-27 21:19:11 +00:00
Laurent Cozic
795568d8c2 Electron: Fixed note sorting bug 2018-02-27 21:11:53 +00:00
Laurent Cozic
df4933fddd All: Prevent export of encrypted items 2018-02-27 20:51:07 +00:00
Laurent Cozic
4046a51472 Electron: Handle import export 2018-02-27 20:04:38 +00:00
Laurent Cozic
45845f645d Refactored Interop service to make export/import code more modular 2018-02-26 19:25:54 +00:00
Laurent Cozic
d7fd8944f7 Moved Enex import to InteropService 2018-02-26 19:16:01 +00:00
Laurent Cozic
3cee671f25 Done MD importer 2018-02-26 18:43:50 +00:00
Laurent Cozic
8f2e5faff3 Support importing JEX and raw data 2018-02-25 17:01:16 +00:00
Laurent Cozic
39ddd934f6 Changed export format extension to JEX and made it a non-compressed tar file 2018-02-25 21:08:40 +00:00
Laurent Cozic
9f8a46b9d9 All: Added more export options including jpz format 2018-02-23 19:32:19 +00:00
Laurent Cozic
c6698eaea6 Electron: Fixes #256: Check that no other instance of Joplin is running before launching a new one 2018-02-23 17:51:23 +00:00
Laurent Cozic
8a96cf3434 All: Allow sorting notes by various fields 2018-02-22 18:58:15 +00:00
Laurent Cozic
74d255c056 Updated readme 2018-02-21 18:19:22 +00:00
Laurent Cozic
71aa841265 Mobile: Fixes #244: When accessing configuration or encrypt configuration option while making a note and the going back, the note gets erased 2018-02-21 22:08:34 +00:00
Laurent Cozic
14a93a9f26 All: Fixed sync interval sorting order 2018-02-21 19:58:28 +00:00
Laurent Cozic
e1fd9c6922 Clean up 2018-02-21 19:20:33 +00:00
Laurent Cozic
b9db747b5c Minor refactoring to make function purpose clearer 2018-02-21 18:36:29 +00:00
Laurent Cozic
4a56c76901 Electron: Fixes #247: Unreadable error messages when checking for updates 2018-02-20 18:07:31 +00:00
Laurent Cozic
6bb3184a72 macOS: Resolves #243: Added black and white tray icon for macOS 2018-02-20 00:41:52 +00:00
Laurent Cozic
7fb8fbd450 iOS: Added compatibility with iPad 2018-02-19 23:41:53 +00:00
Laurent Cozic
9d5bba472e iOS v1.0.13 2018-02-19 23:41:12 +00:00
Laurent Cozic
e6d821a45f CLI v1.0.99 2018-02-19 22:59:09 +00:00
Laurent Cozic
72f0027e21 Electron release v1.0.67 2018-02-19 21:05:43 +00:00
Laurent Cozic
29a13a9943 Android release v1.0.101 2018-02-19 21:04:27 +00:00
Laurent Cozic
3691ae4d13 Electron: Fixes #217: Display a message when the note has no content and only the note viewer is visible 2018-02-19 18:56:56 +00:00
Laurent Cozic
4dda397c29 Removed es_CR translation as it's unfortunately too incomplete and there's already a 100% Spanish translation 2018-02-19 18:44:41 +00:00
Laurent Cozic
b4b058998d Updated French and Español translation 2018-02-19 18:41:39 +00:00
Laurent Cozic
10919e415e Merge branch 'master' of github.com:laurent22/joplin 2018-02-19 18:38:56 +00:00
Laurent Cozic
4966d74864 All: Fixes #240: Tags should be handled in a case-insensitive way 2018-02-19 18:38:27 +00:00
Laurent Cozic
c70ecb30a5 All: Fixes #241: Ignore response for certain calls. Also re-enabled If-None-Match fix. 2018-02-19 18:37:44 +00:00
Laurent Cozic
acc0d17e0f Minor changes 2018-02-19 18:36:31 +00:00
Laurent Cozic
b509b878bf Merge pull request #238 from fmrtn/master
Improved Spanish translations
2018-02-19 18:02:58 +00:00
Laurent Cozic
322ec2efa1 Android release v1.0.100 2018-02-18 22:58:37 +00:00
Laurent Cozic
1232661b1e Electron release v1.0.66 2018-02-18 22:56:57 +00:00
Laurent Cozic
c46d123503 All: Fixed: Local items were no longer being deleted via sync. Also fixed Nextcloud sync target. 2018-02-18 21:52:07 +00:00
Laurent Cozic
8f4060999f Merge branch 'master' of github.com:laurent22/joplin 2018-02-18 21:16:26 +00:00
Laurent Cozic
0addd86069 iOS: Improved compatbility with some WebDAV services (Seafile) 2018-02-18 21:16:18 +00:00
Fernando
760086307b Improved Spanish translations 2018-02-18 17:15:18 +01:00
Laurent Cozic
fc6558a64c Electron release v1.0.65 2018-02-17 19:29:26 +00:00
Laurent Cozic
eca500880d All: Convert new lines in tables to BR tag, and added support for HTML tags in Markdown viewers 2018-02-17 19:29:10 +00:00
Laurent Cozic
90bcd7c977 Merge pull request #226 from fmrtn/master
Nextcloud typos and Spanish update
2018-02-17 11:05:25 +00:00
Fernando
cca0c6eaf3 Updated Spanish translation 2018-02-17 01:09:32 +01:00
Fernando
b0736002be Fixed several Nextcloud typos 2018-02-17 01:09:00 +01:00
Laurent Cozic
51fc2d8e51 CLI v1.0.98 2018-02-16 23:49:22 +00:00
Laurent Cozic
d87c192ff1 Updated readme and translation 2018-02-16 23:26:31 +00:00
Laurent Cozic
52ccf398a6 Update readme 2018-02-16 23:17:55 +00:00
Laurent Cozic
344d0e2687 Update readme 2018-02-16 23:14:26 +00:00
Laurent Cozic
1bc4d6b423 CLI v1.0.97 2018-02-16 23:08:57 +00:00
Laurent Cozic
baa9ca7ea3 Updated terminal readme 2018-02-16 23:08:04 +00:00
Laurent Cozic
e4d477fb4c CLI: Resolve #63: Allow customizing shortcuts 2018-02-16 22:53:53 +00:00
Laurent Cozic
71319eee28 Also add link to po file in translation table 2018-02-16 21:45:28 +00:00
Laurent Cozic
68b31526f8 Updated translations 2018-02-16 21:39:54 +00:00
Laurent Cozic
0b2b7324d9 Merge pull request #224 from fmrtn/master
Fixed and updated Spanish translation
2018-02-16 20:35:46 +00:00
Fernando
43512cf27b Fixed and updated Spanish translation 2018-02-16 20:36:58 +01:00
Laurent Cozic
4218b65969 Electron: Added several keyboard shortcuts 2018-02-16 18:08:02 +00:00
Laurent Cozic
7244e12b78 Electron: Fixed: Confirmation message boxes, and release notes text 2018-02-16 18:06:02 +00:00
Laurent Cozic
a796ef5c66 All: Fixed: Notify DecryptionWorker when item added due to conflict, to make sure it is decrypted as early as possible 2018-02-16 17:55:50 +00:00
Laurent Cozic
9474a05aaa Update readme 2018-02-16 00:17:05 +00:00
Laurent Cozic
41df355a7e CLI v1.0.96 2018-02-15 23:51:00 +00:00
Laurent Cozic
4f3ab87914 Electron release v1.0.64 2018-02-15 23:21:47 +00:00
Laurent Cozic
5d1a08707c Android release v1.0.98 2018-02-15 23:20:28 +00:00
Laurent Cozic
4f822df80e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 23:05:34 +00:00
Laurent Cozic
951be5cbf6 Electron: Fixes #201, Fixes #216: Make sure only one update check can run at a time, and improved modal dialog boxes 2018-02-15 23:05:04 +00:00
Laurent Cozic
b6c2341542 Merge pull request #219 from fmrtn/patch-1
Update README.md
2018-02-15 18:33:48 +00:00
Laurent Cozic
a6e6b49a9d Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:33:21 +00:00
Laurent Cozic
3a4bbd571e All: Provide Content-Length header for WebDAV for better compatibility with more servers 2018-02-15 18:33:08 +00:00
Laurent Cozic
feccc6150e Merge branch 'master' of github.com:laurent22/joplin 2018-02-15 18:19:11 +00:00
Laurent Cozic
a37b599a6b macOS: Allow copy and paste from config and encryption screen 2018-02-15 18:19:00 +00:00
Laurent Cozic
9347683fe3 All: Fixed Nextcloud sync target ID (which was incorrectly set to WebDAV sync ID) 2018-02-15 18:01:05 +00:00
Laurent Cozic
3551c26e28 Fixed race condition when testing with memory driver and fixed encoding issue 2018-02-15 17:12:09 +00:00
Fernando
cfca0107eb Update README.md 2018-02-15 13:09:59 +01:00
rtmkrlv
dfbe37fdaf Merge pull request #3 from laurent22/master
Update fork from original repository
2018-02-15 10:37:59 +02:00
Laurent Cozic
81bc975193 Update readme 2018-02-14 21:54:34 +00:00
Laurent Cozic
7908fda451 Android release v1.0.97 2018-02-14 19:11:35 +00:00
Laurent Cozic
cdbb7c4b0d Merge branch 'master' of github.com:laurent22/joplin 2018-02-14 19:10:42 +00:00
Laurent Cozic
414e57ec55 Electron release v1.0.63 2018-02-14 19:08:24 +00:00
Laurent Cozic
1871123066 All: Improved WebDAV driver compatibility with some services (eg. Seafile) 2018-02-14 19:08:07 +00:00
Laurent Cozic
87bc08bef5 iOS: Fixed resource decryption issue, log page crash and text input rendering issue 2018-02-14 15:28:56 +00:00
Laurent Cozic
214a39c3d3 All: Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken. 2018-02-13 18:26:33 +00:00
Laurent Cozic
ef0cc5e33e Update readme 2018-02-12 20:23:16 +00:00
Laurent Cozic
3a1fa583ab CLI v1.0.95 2018-02-12 18:07:20 +00:00
Laurent Cozic
c1161ae017 Android release v1.0.95 2018-02-12 17:58:55 +00:00
Laurent Cozic
1023ec6206 Electron release v1.0.62 2018-02-12 17:56:42 +00:00
Laurent Cozic
7841421c0d All: Fixes #209: Items with non-ASCII characters end up truncated on Nextcloud 2018-02-12 18:15:22 +00:00
Laurent Cozic
995d8c35dd Updated readme 2018-02-11 20:49:28 +00:00
Laurent Cozic
b179471eff Electron: Fixes #205: Importing Evernote notes while on import page re-imports previous import 2018-02-11 20:31:26 +00:00
Laurent Cozic
19a126ebfe Android release v1.0.94 2018-02-11 17:29:09 +00:00
Laurent Cozic
7e56e5b587 Mobile: Fixes #207: Crash when changing dropdown value in config screen 2018-02-11 17:26:20 +00:00
Laurent Cozic
acf0c79341 Graduated E2EE and WebDAV from beta, and moved to v1.0 2018-02-11 13:50:59 +00:00
Laurent Cozic
9fe7e23ffe Android release v0.10.92 2018-02-11 13:20:32 +00:00
Laurent Cozic
c94cc93971 Fixed translator names 2018-02-10 13:03:01 +00:00
Laurent Cozic
b26094eba8 Fixed Basque flag 2018-02-10 12:52:57 +00:00
Laurent Cozic
89a5ccdf93 Added Basque translation, fixed issue with handling invalid translations. Updated translation FR. 2018-02-10 12:43:45 +00:00
Laurent Cozic
ce2da0e6dc Update readme 2018-02-09 16:03:43 +00:00
Laurent Cozic
f49d644b6a Electron release v0.10.61 2018-02-08 18:15:49 +00:00
Laurent Cozic
02ac0b8593 Removed uneeded created_time property 2018-02-07 20:42:52 +00:00
Laurent Cozic
78e5eaf1e2 Electron: Toolbar button to set tags 2018-02-07 20:35:11 +00:00
Laurent Cozic
fc0d227396 Electron: Allowing opening and saving resource images 2018-02-07 20:23:17 +00:00
Laurent Cozic
f91c52cdf7 Mobile: Update time when app is activated 2018-02-07 19:51:58 +00:00
Laurent Cozic
3f14878d0f All: Improved request repeating mechanism 2018-02-07 19:46:07 +00:00
Laurent Cozic
69fd32e7c6 All: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time 2018-02-07 19:02:07 +00:00
Laurent Cozic
80801cedf0 Android release v0.10.91 2018-02-07 17:57:08 +00:00
Laurent Cozic
480e4fa94b Merge branch 'master' of github.com:laurent22/joplin 2018-02-07 17:56:36 +00:00
Laurent Cozic
717c789836 Android release v0.10.90 2018-02-07 17:55:54 +00:00
Laurent Cozic
f099376446 Update README.md 2018-02-07 17:55:17 +00:00
Laurent Cozic
41fa9d093e Android release v0.10.89 2018-02-07 17:54:06 +00:00
Laurent Cozic
e2f3f81eb6 Android release v0.10.88 2018-02-07 17:51:31 +00:00
Laurent Cozic
5cab7aeb55 Fixed: Make sure alarms and resources are attached to right note when creating new note 2018-02-06 19:31:22 +00:00
Laurent Cozic
fa5f418c22 All: Added sync config check to config screens 2018-02-06 18:59:36 +00:00
Laurent Cozic
a25fcacace Electron: Display message when creating new note or to-do 2018-02-06 18:12:43 +00:00
Laurent Cozic
727ba7300e All: Also support $ as delimiter for Katex expressoins 2018-02-06 17:58:54 +00:00
Laurent Cozic
d25d9b3f44 CLI v0.10.93 2018-02-06 13:17:10 +00:00
Laurent Cozic
9d762a4319 Android release v0.10.86 2018-02-06 13:01:20 +00:00
Laurent Cozic
18d94c7585 Electron release v0.10.60 2018-02-06 12:57:40 +00:00
Laurent Cozic
af82345eb8 Fixed tray icon and update issue 2018-02-06 13:11:59 +00:00
Laurent Cozic
1e94a22986 Merge branch 'master' of github.com:laurent22/joplin 2018-02-06 13:06:59 +00:00
Laurent Cozic
e19a8a99ff macOS: Fixed startup crash 2018-02-06 13:05:36 +00:00
Laurent Cozic
f975009e24 Update readme 2018-02-05 19:02:11 +00:00
Laurent Cozic
90640fafc7 Merge branch 'master' of github.com:laurent22/joplin 2018-02-05 18:49:58 +00:00
Laurent Cozic
42e0e1e5a5 Updated build instructions 2018-02-06 09:42:20 +00:00
Laurent Cozic
61f64fa933 Added Markdown doc 2018-02-05 22:53:10 +00:00
Laurent Cozic
0d0ffd6d27 Added Markdown doc 2018-02-05 22:50:17 +00:00
Laurent Cozic
023ccffd2e Electron release v0.10.59 2018-02-05 22:20:31 +00:00
Laurent Cozic
7257a71a18 Mobile: Fixes #159: Make sure text fields aren't hidden by keyboard on iOS 2018-02-05 18:32:59 +00:00
rtmkrlv
37e7ea0b52 Merge pull request #2 from laurent22/master
Update fork from original repository
2018-02-01 14:43:34 +02:00
rtmkrlv
44bf518244 Revert "Updated Russian translation"
This reverts commit 63cb9b4968.
2018-01-06 14:36:37 +02:00
rtmkrlv
63cb9b4968 Updated Russian translation
Added translation of new strings and small corrections
2018-01-06 14:33:32 +02:00
rtmkrlv
a6cecc103c Merge pull request #1 from laurent22/master
Update
2018-01-06 13:43:28 +02:00
1664 changed files with 150365 additions and 18841 deletions

2
.gitignore vendored Executable file → Normal file
View File

@@ -39,5 +39,7 @@ node_modules
Tools/github_oauth_token.txt
_releases
ReactNativeClient/lib/csstojs/
ReactNativeClient/lib/rnInjectedJs/
ElectronClient/app/gui/note-viewer/fonts/
ElectronClient/app/gui/note-viewer/lib.js
Tools/commit_hook.txt

View File

@@ -3,6 +3,14 @@ if: tag IS present
rvm: 2.3.3
# It's important to only build production branches otherwise Electron Builder
# might take assets from dev branches and overwrite those of production.
# https://docs.travis-ci.com/user/customizing-the-build/#Building-Specific-Branches
branches:
only:
- master
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/
matrix:
include:
- os: osx

62
Assets/JoplinLetter.svg Normal file
View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="116.54575mm"
height="131.19589mm"
viewBox="0 0 116.54575 131.19589"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="JoplinLetter.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="152.11122"
inkscape:cy="-26.090631"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-2.7903623,-2.175533)">
<path
style="fill:#000000;stroke-width:0.26458332"
d="m 43.790458,133.13317 c -8.32317,-1.11843 -12.937,-2.40956 -18.46857,-5.16822 -10.21924,-5.09644 -18.1023498,-13.95338 -21.1745998,-23.79038 -1.22214,-3.91319 -1.3607,-4.872332 -1.35685,-9.392712 0.003,-3.72804 0.0907,-4.66941 0.59927,-6.44569 1.0664,-3.7246 2.49409,-6.1704 5.19529,-8.90014 3.2574198,-3.29184 6.6565798,-4.77332 11.3929598,-4.96548 4.53189,-0.18388 7.54661,0.59927 10.40386,2.70266 1.82035,1.34007 3.67693,3.96421 4.71565,6.66525 0.65839,1.71204 0.70959,2.1839 0.90042,8.29756 0.19973,6.39855 0.36372,7.6318 1.39223,10.469902 1.40468,3.87611 3.78939,6.56189 7.33039,8.25588 3.20047,1.53108 5.63801,2.00183 9.60817,1.8556 2.58182,-0.0951 3.60332,-0.25442 5.15337,-0.80371 4.61358,-1.63493 8.46322,-5.31381 10.31326,-9.85579 1.91154,-4.693002 1.90785,-4.609372 1.90213,-43.127082 -0.005,-33.78395 -0.0106,-34.14337 -0.54484,-35.32188 -1.30698,-2.882895 -2.68223,-3.398165 -9.66971,-3.622945 l -5.12472,-0.16486 V 10.998334 2.175533 l 31.41927,0.06723 31.419272,0.06723 0.0697,8.755726 0.0697,8.755724 -5.09675,0.1793 c -2.82759,0.0995 -5.60596,0.33101 -6.24051,0.52006 -1.72896,0.5151 -2.82899,1.538795 -3.52569,3.281045 l -0.61059,1.5269 -0.16762,34.7927 c -0.16988,35.26321 -0.19381,36.08914 -1.18496,40.914372 -1.81292,8.82581 -8.301582,17.89221 -16.959672,23.69719 -6.95182,4.66099 -14.48972,7.21214 -24.82645,8.40235 -2.7431,0.31585 -14.57797,0.31433 -16.93333,-0.002 z"
id="path21"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -1,15 +1,14 @@
[![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)
# 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.
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
## macOS dependencies
brew install yarn node
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
## Linux and Windows (WSL) dependencies
@@ -17,6 +16,15 @@ If you get a node-gyp related error you might need to manually install it: `npm
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
Before building any of the applications, you need to build the tools:
```
cd Tools
npm install
```
# Building the Electron application
```
@@ -28,12 +36,31 @@ yarn dist
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
For node-gyp to work, you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`.
If you get the error `libtool: unrecognized option '-static'`, follow the instructions [in this post](https://stackoverflow.com/a/38552393/561309) to use the correct libtool version.
That will create the executable file in the `dist` directory.
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
## Building Electron application on Windows
```
cd Tools
npm install
cd ..\ElectronClient\app
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
npm install
yarn dist
```
If node-gyp does not works (MSBUILD: error MSB3428: Could not load the Visual C++ component "VCBuild.exe"), you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
If `yarn dist` fails, it may need administrative rights.
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
# Building the Mobile application
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab.

View File

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

View File

@@ -13,9 +13,11 @@ tests/fuzzing.*
tests/fuzzing -*
tests/logs/*
tests/cli-integration/
tests/tmp/
*.mo
*.*~
tests/sync
out.txt
linkToLocal.sh
yarn-error.log
yarn-error.log
tests/support/dropbox-auth.txt

View File

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

View File

@@ -5,6 +5,7 @@ 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');
@@ -20,8 +21,9 @@ const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/loc
const os = require('os');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const EventEmitter = require('events');
const Cache = require('lib/Cache');
const WelcomeUtils = require('lib/WelcomeUtils');
const RevisionService = require('lib/services/RevisionService');
class Application extends BaseApplication {
@@ -283,7 +285,7 @@ class Application extends BaseApplication {
exit: () => {},
showModalOverlay: (text) => {},
hideModalOverlay: () => {},
stdoutMaxWidth: () => { return 78; },
stdoutMaxWidth: () => { return 100; },
forceRender: () => {},
termSaveState: () => {},
termRestoreState: (state) => {},
@@ -292,7 +294,7 @@ class Application extends BaseApplication {
async execCommand(argv) {
if (!argv.length) return this.execCommand(['help']);
reg.logger().info('execCommand()', argv);
// reg.logger().debug('execCommand()', argv);
const commandName = argv[0];
this.activeCommand_ = this.findCommandByName(commandName);
@@ -312,6 +314,63 @@ class Application extends BaseApplication {
return this.activeCommand_;
}
async loadKeymaps() {
const defaultKeyMap = [
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
{ "keys": ["UP"], "type": "function", "command": "move_up" },
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
];
// Filter the keymap item by command so that items in keymap.json can override
// the default ones.
const itemsByCommand = {};
for (let i = 0; i < defaultKeyMap.length; i++) {
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
}
const filePath = Setting.value('profileDir') + '/keymap.json';
if (await fs.pathExists(filePath)) {
try {
let configString = await fs.readFile(filePath, 'utf-8');
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
const keymap = JSON.parse(configString);
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
const item = keymap[keymapIndex];
itemsByCommand[item.command] = item;
}
} catch (error) {
let msg = error.message ? error.message : '';
msg = 'Could not load keymap ' + filePath + '\n' + msg;
error.message = msg;
throw error;
}
}
const output = [];
for (let n in itemsByCommand) {
if (!itemsByCommand.hasOwnProperty(n)) continue;
output.push(itemsByCommand[n]);
}
return output;
}
async start(argv) {
argv = await super.start(argv);
@@ -319,6 +378,8 @@ class Application extends BaseApplication {
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) {
@@ -330,16 +391,25 @@ class Application extends BaseApplication {
await this.execCommand(argv);
} catch (error) {
if (this.showStackTraces_) {
console.info(error);
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());
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
await this.gui_.start();
@@ -352,6 +422,10 @@ class Application extends BaseApplication {
const tags = await Tag.allWithNotes();
ResourceService.runInBackground();
RevisionService.instance().runInBackground();
this.dispatch({
type: 'TAG_UPDATE_ALL',
items: tags,

View File

@@ -36,7 +36,7 @@ async function handleAutocompletionPromise(line) {
if (next[0] === '-') {
for (let i = 0; i<metadata.options.length; i++) {
const options = metadata.options[i][0].split(' ');
//if there are multiple options then they will be seperated by comma and
//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);

View File

@@ -32,10 +32,6 @@ class BaseCommand {
return this.compatibleUis().indexOf(ui) >= 0;
}
aliases() {
return [];
}
options() {
return [];
}

View File

@@ -102,7 +102,7 @@ function getFooter() {
output.push('WEBSITE');
output.push('');
output.push(INDENT + 'http://joplin.cozic.net');
output.push(INDENT + 'https://joplinapp.org');
output.push('');

View File

@@ -0,0 +1,299 @@
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

@@ -26,7 +26,7 @@ class Command extends BaseCommand {
const md = Setting.settingMetadata(name);
let value = Setting.value(name);
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
if (md.secure) value = '********';
if (md.secure && value) value = '********';
if (Setting.isEnum(name)) {
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));

View File

@@ -6,6 +6,10 @@ 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 {
@@ -14,7 +18,7 @@ class Command extends BaseCommand {
}
description() {
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.');
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file` and `target-status`.');
}
options() {
@@ -22,6 +26,7 @@ class Command extends BaseCommand {
// 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'],
];
}
@@ -30,6 +35,18 @@ class Command extends BaseCommand {
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) {
@@ -47,30 +64,29 @@ class Command extends BaseCommand {
}
if (args.command === 'decrypt') {
this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to 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 masterKeyId = error.masterKeyId;
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
if (!password) {
this.stdout(_('Operation cancelled'));
return;
while (true) {
try {
await DecryptionWorker.instance().start();
break;
} catch (error) {
if (error.code === 'masterKeyNotLoaded') {
const ok = await askForMasterKey(error);
if (!ok) return;
continue;
}
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
await EncryptionService.instance().loadMasterKeysFromSettings();
continue;
throw error;
}
throw error;
}
}
this.stdout(_('Completed decryption.'));
this.stdout(_('Completed decryption.'));
}
return;
}
@@ -80,6 +96,36 @@ class Command extends BaseCommand {
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');

View File

@@ -1,5 +1,5 @@
const { BaseCommand } = require('./base-command.js');
const { Exporter } = require('lib/services/exporter.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');
@@ -10,15 +10,21 @@ const fs = require('fs-extra');
class Command extends BaseCommand {
usage() {
return 'export <directory>';
return 'export <path>';
}
description() {
return _('Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.');
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.')],
];
@@ -26,13 +32,9 @@ class Command extends BaseCommand {
async action(args) {
let exportOptions = {};
exportOptions.destDir = args.directory;
exportOptions.writeFile = (filePath, data) => {
return fs.writeFile(filePath, data);
};
exportOptions.copyFile = (source, dest) => {
return fs.copy(source, dest, { overwrite: true });
};
exportOptions.path = args.path;
exportOptions.format = args.options.format ? args.options.format : 'jex';
if (args.options.note) {
@@ -48,10 +50,10 @@ class Command extends BaseCommand {
}
const exporter = new Exporter();
const result = await exporter.export(exportOptions);
const service = new InteropService();
const result = await service.export(exportOptions);
reg.logger().info('Export result: ', result);
result.warnings.map((w) => this.stdout(w));
}
}

View File

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

View File

@@ -0,0 +1,75 @@
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

@@ -22,7 +22,7 @@ class Command extends BaseCommand {
enabled() {
return false;
}
options() {
return [
['-n, --limit <num>', _('Displays only the first top <num> notes.')],
@@ -93,7 +93,7 @@ class Command extends BaseCommand {
row.push(await Folder.noteCount(item.id));
}
row.push(time.unixMsToLocalDateTime(item.user_updated_time));
row.push(time.formatMsToLocal(item.user_updated_time));
}
let title = item.title;
@@ -123,4 +123,4 @@ class Command extends BaseCommand {
}
module.exports = Command;
module.exports = Command;

View File

@@ -14,10 +14,6 @@ class Command extends BaseCommand {
return _('Creates a new notebook.');
}
aliases() {
return ['mkdir'];
}
async action(args) {
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
app().switchCurrentFolder(folder);

View File

@@ -29,7 +29,7 @@ class Command extends BaseCommand {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook? All notes within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
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);

View File

@@ -49,35 +49,6 @@ class Command extends BaseCommand {
type: 'SEARCH_SELECT',
id: searchId,
});
// let fields = Note.previewFields();
// fields.push('body');
// const notes = await Note.previews(folder ? folder.id : null, {
// fields: fields,
// anywherePattern: '*' + pattern + '*',
// });
// const fragmentLength = 50;
// let parents = {};
// for (let i = 0; i < notes.length; i++) {
// const note = notes[i];
// const parent = parents[note.parent_id] ? parents[note.parent_id] : await Folder.load(note.parent_id);
// parents[note.parent_id] = parent;
// const idx = note.body.indexOf(pattern);
// let line = '';
// if (idx >= 0) {
// let fragment = note.body.substr(Math.max(0, idx - fragmentLength / 2), fragmentLength);
// fragment = fragment.replace(/\n/g, ' ');
// line = sprintf('%s: %s / %s: %s', BaseModel.shortId(note.id), parent.title, note.title, fragment);
// } else {
// line = sprintf('%s: %s / %s', BaseModel.shortId(note.id), parent.title, note.title);
// }
// this.stdout(line);
// }
}
}

View File

@@ -4,13 +4,13 @@ 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');
const { cliUtils } = require('./cli-utils.js');
const md5 = require('md5');
const locker = require('proper-lockfile');
const fs = require('fs-extra');
const osTmpdir = require('os-tmpdir');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class Command extends BaseCommand {
@@ -79,10 +79,26 @@ class Command extends BaseCommand {
return false;
}
return true;
} 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:'));
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
this.stdout(loginUrl);
const authCode = await this.prompt(_('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
if (!authCode) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return false;
}
const response = await api.execAuthToken(authCode);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
api.setAuthToken(response.access_token);
return true;
}
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTargetMd.label));
return false;
}
@@ -101,7 +117,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
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 {
@@ -131,7 +147,7 @@ class Command extends BaseCommand {
const syncTarget = reg.syncTarget(this.syncTargetId_);
if (!syncTarget.isAuthenticated()) {
if (!await syncTarget.isAuthenticated()) {
app().gui().showConsole();
app().gui().maximizeConsole();
@@ -175,6 +191,15 @@ class Command extends BaseCommand {
}
}
// When using the tool in command line mode, the ResourceFetcher service is
// not going to be running in the background, so the resources need to be
// explicitely downloaded below.
if (!app().hasGui()) {
this.stdout(_('Downloading resources...'));
await ResourceFetcher.instance().fetchAll();
await ResourceFetcher.instance().waitForAllFinished();
}
await app().refreshCurrentFolder();
} catch (error) {
cleanUp();
@@ -198,7 +223,7 @@ class Command extends BaseCommand {
const syncTarget = reg.syncTarget(syncTargetId);
if (syncTarget.isAuthenticated()) {
if (await syncTarget.isAuthenticated()) {
const sync = await syncTarget.synchronizer();
if (sync) await sync.cancel();
} else {

View File

@@ -3,6 +3,7 @@ const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
@@ -11,11 +12,19 @@ class Command extends BaseCommand {
}
description() {
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.');
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags (use -l for long option).');
}
options() {
return [
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
}
async action(args) {
let tag = null;
let options = args.options;
if (args.tag) tag = await app().loadItem(BaseModel.TYPE_TAG, args.tag);
let notes = [];
if (args.note) {
@@ -41,7 +50,28 @@ class Command extends BaseCommand {
} else if (command == 'list') {
if (tag) {
let notes = await Tag.notes(tag.id);
notes.map((note) => { this.stdout(note.title); });
notes.map((note) => {
let line = '';
if (options.long) {
line += BaseModel.shortId(note.id);
line += ' ';
line += time.formatMsToLocal(note.user_updated_time);
line += ' ';
}
if (note.is_todo) {
line += '[';
if (note.todo_completed) {
line += 'X';
} else {
line += ' ';
}
line += '] ';
} else {
line += ' ';
}
line += note.title;
this.stdout(line);
});
} else {
let tags = await Tag.all();
tags.map((tag) => { this.stdout(tag.title); });

View File

@@ -18,26 +18,36 @@ class FolderListWidget extends ListWidget {
this.notesParentType_ = 'Folder';
this.updateIndexFromSelectedFolderId_ = false;
this.updateItems_ = false;
this.trimItemTitle = false;
this.itemRenderer = (item) => {
let output = [];
if (item === '-') {
output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) {
output.push(Folder.displayTitle(item));
output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
} else if (item.type_ === Tag.modelType()) {
output.push('[' + Folder.displayTitle(item) + ']');
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:'));
output.push(item.title);
}
// if (item && item.id) output.push(item.id.substr(0, 5));
}
return output.join(' ');
};
}
folderDepth(folders, folderId) {
let output = 0;
while (true) {
const folder = BaseModel.byId(folders, folderId);
if (!folder.parent_id) return output;
output++;
folderId = folder.parent_id;
}
throw new Error('unreachable');
}
get selectedFolderId() {
return this.selectedFolderId_;
}
@@ -73,7 +83,6 @@ class FolderListWidget extends ListWidget {
}
set notesParentType(v) {
//if (this.notesParentType_ === v) return;
this.notesParentType_ = v;
this.updateIndexFromSelectedItemId()
this.invalidate();
@@ -111,6 +120,14 @@ class FolderListWidget extends ListWidget {
this.updateIndexFromSelectedItemId()
this.invalidate();
}
folderHasChildren_(folders, folderId) {
for (let i = 0; i < folders.length; i++) {
let folder = folders[i];
if (folder.parent_id === folderId) return true;
}
return false;
}
render() {
if (this.updateItems_) {
@@ -118,7 +135,19 @@ class FolderListWidget extends ListWidget {
const wasSelectedItemId = this.selectedJoplinItemId;
const previousParentType = this.notesParentType;
let newItems = this.folders.slice();
let newItems = [];
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 : '';
if (folderParentId === parentId) {
newItems.push(f);
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
}
}
}
orderFolders('');
if (this.tags.length) {
if (newItems.length) newItems.push('-');

View File

@@ -133,7 +133,8 @@ class StatusBarWidget extends BaseWidget {
resolveResult = input ? input.trim() : input;
// Add the command to history but only if it's longer than one character.
// Below that it's usually an answer like "y"/"n", etc.
if (!isSecurePrompt && input && input.length > 1) this.history_.push(input);
const isConfigPassword = input.indexOf('config ') >= 0 && input.indexOf('password') >= 0;
if (!isSecurePrompt && input && input.length > 1 && !isConfigPassword) this.history_.push(input);
}
}

View File

@@ -53,9 +53,8 @@ function renderCommandHelp(cmd, width = null) {
desc.push(label);
}
if (md.description) {
desc.push(md.description());
}
const description = Setting.keyDescription(md.key, 'cli');
if (description) desc.push(description);
desc.push(_('Type: %s.', md.isEnum ? _('Enum') : Setting.typeToString(md.type)));
if (md.isEnum) desc.push(_('Possible values: %s.', Setting.enumOptionsDoc(md.key, '%s (%s)')));

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env node
// Use njstrace to find out what Node.js might be spending time on
// var njstrace = require('njstrace').inject();
// Make it possible to require("/lib/...") without specifying full path
require('app-module-path').addPath(__dirname);
@@ -19,6 +22,7 @@ const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting.js');
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
@@ -40,6 +44,7 @@ BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');
@@ -66,54 +71,14 @@ process.stdout.on('error', function( err ) {
}
});
// async function main() {
// const WebDavApi = require('lib/WebDavApi');
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
// const driver = new FileApiDriverWebDav(api);
// const stat = await driver.stat('');
// console.info(stat);
// // const stat = await driver.stat('testing.txt');
// // console.info(stat);
// // const content = await driver.get('testing.txta');
// // console.info(content);
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
// // console.info(content);
// // const content = await driver.mkdir('newdir5');
// // console.info(content);
// //await driver.put('myfile4.md', 'this is my content');
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
// // await driver.delete('myfile4.md');
// // const deltaResult = await driver.delta('', {
// // allItemIdsHandler: () => { return []; }
// // });
// // console.info(deltaResult);
// }
// main().catch((error) => { console.error(error); });
application.start(process.argv).catch((error) => {
console.error(_('Fatal error:'));
console.error(error);
if (error.code == 'flagError') {
console.error(error.message);
console.error(_('Type `joplin help` for usage information.'));
} else {
console.error(_('Fatal error:'));
console.error(error);
}
process.exit(1);
});

View File

@@ -0,0 +1,405 @@
# 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

@@ -0,0 +1,97 @@
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

@@ -0,0 +1,822 @@
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

@@ -0,0 +1,451 @@
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

@@ -0,0 +1,199 @@
var { app } = require('./app.js');
var Note = require('lib/models/Note.js');
var Folder = require('lib/models/Folder.js');
var Tag = require('lib/models/Tag.js');
var { cliUtils } = require('./cli-utils.js');
var yargParser = require('yargs-parser');
var fs = require('fs-extra');
async function handleAutocompletionPromise(line) {
// Auto-complete the command name
const names = await app().commandNames();
let words = getArguments(line);
//If there is only one word and it is not already a command name then you
//should look for commmands it could be
if (words.length == 1) {
if (names.indexOf(words[0]) === -1) {
let x = names.filter((n) => n.indexOf(words[0]) === 0);
if (x.length === 1) {
return x[0] + ' ';
}
return x.length > 0 ? x.map((a) => a + ' ') : line;
} else {
return line;
}
}
//There is more than one word and it is a command
const metadata = (await app().commandMetadata())[words[0]];
//If for some reason this command does not have any associated metadata
//just don't autocomplete. However, this should not happen.
if (metadata === undefined) {
return line;
}
//complete an option
let next = words.length > 1 ? words[words.length - 1] : '';
let l = [];
if (next[0] === '-') {
for (let i = 0; i<metadata.options.length; i++) {
const options = metadata.options[i][0].split(' ');
//if there are multiple options then they will be 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

@@ -0,0 +1,99 @@
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

@@ -0,0 +1,139 @@
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

@@ -0,0 +1,249 @@
"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

@@ -0,0 +1,250 @@
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

@@ -0,0 +1,299 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,70 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,230 @@
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

@@ -0,0 +1,119 @@
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,91 @@
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

@@ -0,0 +1,75 @@
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

@@ -0,0 +1,126 @@
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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,40 @@
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;

View File

@@ -0,0 +1,41 @@
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>';
}
description() {
return _('Deletes the notes matching <note-pattern>.');
}
options() {
return [
['-f, --force', _('Deletes the notes without asking for confirmation.')],
];
}
async action(args) {
const pattern = args['note-pattern'];
const force = args.options && args.options.force === true;
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
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);
await Note.batchDelete(ids);
}
}
module.exports = Command;

View File

@@ -0,0 +1,56 @@
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]';
}
description() {
return _('Searches for the given <pattern> in all the notes.');
}
compatibleUis() {
return ['gui'];
}
async action(args) {
let pattern = args['pattern'];
let folderTitle = args['notebook'];
let folder = null;
if (folderTitle) {
folder = await Folder.loadByTitle(folderTitle);
if (!folder) throw new Error(_('Cannot find "%s".', folderTitle));
}
const searchId = uuid.create();
this.dispatch({
type: 'SEARCH_ADD',
search: {
id: searchId,
title: pattern,
query_pattern: pattern,
query_folder_id: folder ? folder.id : '',
type_: BaseModel.TYPE_SEARCH,
},
});
this.dispatch({
type: 'SEARCH_SELECT',
id: searchId,
});
}
}
module.exports = Command;

View File

@@ -0,0 +1,51 @@
const { BaseCommand } = require('./base-command.js');
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]';
}
description() {
const fields = Note.fields();
const s = [];
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
if (f.name === 'id') continue;
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
}
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
}
async action(args) {
let title = args['note'];
let propName = args['name'];
let propValue = args['value'];
if (!propValue) propValue = '';
let notes = await app().loadItems(BaseModel.TYPE_NOTE, title);
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
for (let i = 0; i < notes.length; i++) {
this.encryptionCheck(notes[i]);
let newNote = {
id: notes[i].id,
type_: notes[i].type_,
};
newNote[propName] = propValue;
await Note.save(newNote);
}
}
}
module.exports = Command;

View File

@@ -0,0 +1,43 @@
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';
}
description() {
return _('Displays summary about the notes and notebooks.');
}
async action(args) {
let service = new ReportService();
let report = await service.status(Setting.value('sync.target'));
for (let i = 0; i < report.length; i++) {
let section = report[i];
if (i > 0) this.stdout('');
this.stdout('# ' + section.title);
this.stdout('');
for (let n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
let line = section.body[n];
this.stdout(line);
}
}
app().gui().showConsole();
app().gui().maximizeConsole();
}
}
module.exports = Command;

View File

@@ -0,0 +1,243 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
const Setting = require('lib/models/Setting.js');
const BaseItem = require('lib/models/BaseItem.js');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const { Synchronizer } = require('lib/synchronizer.js');
const { reg } = require('lib/registry.js');
const { cliUtils } = require('./cli-utils.js');
const md5 = require('md5');
const locker = require('proper-lockfile');
const fs = require('fs-extra');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class Command extends BaseCommand {
constructor() {
super();
this.syncTargetId_ = null;
this.releaseLockFn_ = null;
this.oneDriveApiUtils_ = null;
}
usage() {
return 'sync';
}
description() {
return _('Synchronises with remote storage.');
}
options() {
return [
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
];
}
static lockFile(filePath) {
return new Promise((resolve, reject) => {
locker.lock(filePath, { stale: 1000 * 60 * 5 }, (error, release) => {
if (error) {
reject(error);
return;
}
resolve(release);
});
});
}
static isLocked(filePath) {
return new Promise((resolve, reject) => {
locker.check(filePath, (error, isLocked) => {
if (error) {
reject(error);
return;
}
resolve(isLocked);
});
});
}
async doAuth() {
const syncTarget = reg.syncTarget(this.syncTargetId_);
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return false;
}
return true;
} 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:'));
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
this.stdout(loginUrl);
const authCode = await this.prompt(_('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
if (!authCode) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return false;
}
const response = await api.execAuthToken(authCode);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
api.setAuthToken(response.access_token);
return true;
}
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTargetMd.label));
return false;
}
cancelAuth() {
if (this.oneDriveApiUtils_) {
this.oneDriveApiUtils_.cancelOAuthDance();
return;
}
}
doingAuth() {
return !!this.oneDriveApiUtils_;
}
async action(args) {
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');
try {
if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.'));
this.releaseLockFn_ = await Command.lockFile(lockFilePath);
} catch (error) {
if (error.code == 'ELOCKED') {
const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file);
this.stdout(msg);
return;
}
throw error;
}
const cleanUp = () => {
cliUtils.redrawDone();
if (this.releaseLockFn_) {
this.releaseLockFn_();
this.releaseLockFn_ = null;
}
};
try {
this.syncTargetId_ = Setting.value('sync.target');
if (args.options.target) this.syncTargetId_ = args.options.target;
const syncTarget = reg.syncTarget(this.syncTargetId_);
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) => {
let lines = Synchronizer.reportToLines(report);
if (lines.length) cliUtils.redraw(lines.join(' '));
},
onMessage: (msg) => {
cliUtils.redrawDone();
this.stdout(msg);
},
};
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
if (!sync) throw new Error(_('Cannot initialize synchroniser.'));
this.stdout(_('Starting synchronisation...'));
const contextKey = 'sync.' + this.syncTargetId_ + '.context';
let context = Setting.value(contextKey);
context = context ? JSON.parse(context) : {};
options.context = context;
try {
let newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) {
if (error.code == 'alreadyStarted') {
this.stdout(error.message);
} else {
throw error;
}
}
// When using the tool in command line mode, the ResourceFetcher service is
// not going to be running in the background, so the resources need to be
// explicitely downloaded below.
if (!app().hasGui()) {
this.stdout(_('Downloading resources...'));
await ResourceFetcher.instance().fetchAll();
await ResourceFetcher.instance().waitForAllFinished();
}
await app().refreshCurrentFolder();
} catch (error) {
cleanUp();
throw error;
}
cleanUp();
}
async cancel() {
if (this.doingAuth()) {
this.cancelAuth();
return;
}
const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting.value('sync.target');
cliUtils.redrawDone();
this.stdout(_('Cancelling... Please wait.'));
const syncTarget = reg.syncTarget(syncTargetId);
if (await syncTarget.isAuthenticated()) {
const sync = await syncTarget.synchronizer();
if (sync) await sync.cancel();
} else {
if (this.releaseLockFn_) this.releaseLockFn_();
this.releaseLockFn_ = null;
}
this.syncTargetId_ = null;
}
cancellable() {
return true;
}
}
module.exports = Command;

View File

@@ -0,0 +1,86 @@
const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const { time } = require('lib/time-utils.js');
class Command extends BaseCommand {
usage() {
return 'tag <tag-command> [tag] [note]';
}
description() {
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags (use -l for long option).');
}
options() {
return [
['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')],
];
}
async action(args) {
let tag = null;
let options = args.options;
if (args.tag) tag = await app().loadItem(BaseModel.TYPE_TAG, args.tag);
let notes = [];
if (args.note) {
notes = await app().loadItems(BaseModel.TYPE_NOTE, args.note);
}
const command = args['tag-command'];
if (command == 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command == 'add') {
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
if (!tag) tag = await Tag.save({ title: args.tag }, { userSideValidation: true });
for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id);
}
} else if (command == 'remove') {
if (!tag) throw new Error(_('Cannot find "%s".', args.tag));
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
for (let i = 0; i < notes.length; i++) {
await Tag.removeNote(tag.id, notes[i].id);
}
} else if (command == 'list') {
if (tag) {
let notes = await Tag.notes(tag.id);
notes.map((note) => {
let line = '';
if (options.long) {
line += BaseModel.shortId(note.id);
line += ' ';
line += time.formatMsToLocal(note.user_updated_time);
line += ' ';
}
if (note.is_todo) {
line += '[';
if (note.todo_completed) {
line += 'X';
} else {
line += ' ';
}
line += '] ';
} else {
line += ' ';
}
line += note.title;
this.stdout(line);
});
} else {
let tags = await Tag.all();
tags.map((tag) => { this.stdout(tag.title); });
}
} else {
throw new Error(_('Invalid command: "%s"', command));
}
}
}
module.exports = Command;

View File

@@ -0,0 +1,50 @@
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>';
}
description() {
return _('<todo-command> can either be "toggle" or "clear". Use "toggle" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use "clear" to convert the to-do back to a regular note.');
}
async action(args) {
const action = args['todo-command'];
const pattern = args['note-pattern'];
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++) {
const note = notes[i];
this.encryptionCheck(note);
let toSave = {
id: note.id,
};
if (action == 'toggle') {
if (!note.is_todo) {
toSave = Note.toggleIsTodo(note);
} else {
toSave.todo_completed = note.todo_completed ? 0 : time.unixMs();
}
} else if (action == 'clear') {
toSave.is_todo = 0;
}
await Note.save(toSave);
}
}
}
module.exports = Command;

View File

@@ -0,0 +1,27 @@
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>';
}
description() {
return _('Marks a to-do as non-completed.');
}
async action(args) {
await CommandDone.handleAction(this, args, false);
}
}
module.exports = Command;

View File

@@ -0,0 +1,33 @@
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>';
}
description() {
return _('Switches to [notebook] - all further operations will happen within this notebook.');
}
autocomplete() {
return { data: autocompleteFolders };
}
compatibleUis() {
return ['cli'];
}
async action(args) {
let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
app().switchCurrentFolder(folder);
}
}
module.exports = Command;

View File

@@ -0,0 +1,22 @@
const { BaseCommand } = require('./base-command.js');
const Setting = require('lib/models/Setting.js');
const { _ } = require('lib/locale.js');
class Command extends BaseCommand {
usage() {
return 'version';
}
description() {
return _('Displays version information');
}
async action(args) {
const p = require('./package.json');
this.stdout(_('%s %s (%s)', p.name, p.version, Setting.value('env')));
}
}
module.exports = Command;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
const TextWidget = require('tkwidgets/TextWidget.js');
class ConsoleWidget extends TextWidget {
constructor() {
super();
this.lines_ = [];
this.updateText_ = false;
this.markdownRendering = false;
this.stickToBottom = true;
this.maxLines_ = 1000;
}
get name() {
return 'console';
}
get lastLine() {
return this.lines_.length ? this.lines_[this.lines_.length-1] : '';
}
addLine(line) {
this.lines_.push(line);
this.updateText_ = true;
this.invalidate();
}
onFocus() {
this.stickToBottom = false;
super.onFocus();
}
onBlur() {
this.stickToBottom = true;
super.onBlur();
}
render() {
if (this.updateText_) {
if (this.lines_.length > this.maxLines_) {
this.lines_.splice(0, this.lines_.length - this.maxLines_);
}
this.text = this.lines_.join("\n");
this.updateText_ = false;
}
super.render();
}
}
module.exports = ConsoleWidget;

View File

@@ -0,0 +1,194 @@
const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js');
const ListWidget = require('tkwidgets/ListWidget.js');
const _ = require('lib/locale.js')._;
class FolderListWidget extends ListWidget {
constructor() {
super();
this.tags_ = [];
this.folders_ = [];
this.searches_ = [];
this.selectedFolderId_ = null;
this.selectedTagId_ = null;
this.selectedSearchId_ = null;
this.notesParentType_ = 'Folder';
this.updateIndexFromSelectedFolderId_ = false;
this.updateItems_ = false;
this.trimItemTitle = false;
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) + ']');
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:'));
output.push(item.title);
}
return output.join(' ');
};
}
folderDepth(folders, folderId) {
let output = 0;
while (true) {
const folder = BaseModel.byId(folders, folderId);
if (!folder.parent_id) return output;
output++;
folderId = folder.parent_id;
}
throw new Error('unreachable');
}
get selectedFolderId() {
return this.selectedFolderId_;
}
set selectedFolderId(v) {
this.selectedFolderId_ = v;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get selectedSearchId() {
return this.selectedSearchId_;
}
set selectedSearchId(v) {
this.selectedSearchId_ = v;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get selectedTagId() {
return this.selectedTagId_;
}
set selectedTagId(v) {
this.selectedTagId_ = v;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get notesParentType() {
return this.notesParentType_;
}
set notesParentType(v) {
this.notesParentType_ = v;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get searches() {
return this.searches_;
}
set searches(v) {
this.searches_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get tags() {
return this.tags_;
}
set tags(v) {
this.tags_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
get folders() {
return this.folders_;
}
set folders(v) {
this.folders_ = v;
this.updateItems_ = true;
this.updateIndexFromSelectedItemId()
this.invalidate();
}
folderHasChildren_(folders, folderId) {
for (let i = 0; i < folders.length; i++) {
let folder = folders[i];
if (folder.parent_id === folderId) return true;
}
return false;
}
render() {
if (this.updateItems_) {
this.logger().debug('Rebuilding items...', this.notesParentType, this.selectedJoplinItemId, this.selectedSearchId);
const wasSelectedItemId = this.selectedJoplinItemId;
const previousParentType = this.notesParentType;
let newItems = [];
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 : '';
if (folderParentId === parentId) {
newItems.push(f);
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
}
}
}
orderFolders('');
if (this.tags.length) {
if (newItems.length) newItems.push('-');
newItems = newItems.concat(this.tags);
}
if (this.searches.length) {
if (newItems.length) newItems.push('-');
newItems = newItems.concat(this.searches);
}
this.items = newItems;
this.notesParentType = previousParentType;
this.updateIndexFromSelectedItemId(wasSelectedItemId)
this.updateItems_ = false;
}
super.render();
}
get selectedJoplinItemId() {
if (!this.notesParentType) return '';
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);
}
get selectedJoplinItem() {
const id = this.selectedJoplinItemId;
const index = this.itemIndexByKey('id', id);
return this.itemAt(index);
}
updateIndexFromSelectedItemId(itemId = null) {
if (itemId === null) itemId = this.selectedJoplinItemId;
const index = this.itemIndexByKey('id', itemId);
this.currentIndex = index >= 0 ? index : 0;
}
}
module.exports = FolderListWidget;

View File

@@ -0,0 +1,38 @@
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) => {
let label = Note.displayTitle(note); // + ' ' + note.id;
if (note.is_todo) {
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
}
return label;
};
}
set selectedNoteId(v) {
this.updateIndexFromSelectedNoteId_ = true;
this.selectedNoteId_ = v;
}
render() {
if (this.updateIndexFromSelectedNoteId_) {
const index = this.itemIndexByKey('id', this.selectedNoteId_);
this.currentIndex = index >= 0 ? index : 0;
this.updateIndexFromSelectedNoteId_ = false;
}
super.render();
}
}
module.exports = NoteListWidget;

View File

@@ -0,0 +1,36 @@
const Note = require('lib/models/Note.js');
const TextWidget = require('tkwidgets/TextWidget.js');
class NoteMetadataWidget extends TextWidget {
constructor() {
super();
this.noteId_ = 0;
this.note_ = null;
}
get noteId() {
return this.noteId_;
}
set noteId(v) {
// If this is called it means either the note ID has changed OR
// the note content has changed, so we always set note_ to null
// so that it can be reloaded in onWillRender().
this.noteId_ = v;
this.note_ = null;
this.invalidate();
}
async onWillRender() {
if (!this.visible) return;
if (!this.note_ && this.noteId_) {
this.note_ = await Note.load(this.noteId_);
this.text = this.note_ ? await Note.minimalSerializeForDisplay(this.note_) : '';
}
}
}
module.exports = NoteMetadataWidget;

View File

@@ -0,0 +1,65 @@
const Note = require('lib/models/Note.js');
const TextWidget = require('tkwidgets/TextWidget.js');
const { _ } = require('lib/locale.js');
class NoteWidget extends TextWidget {
constructor() {
super();
this.noteId_ = 0;
this.note_ = null;
this.notes_ = [];
this.lastLoadedNoteId_ = null;
}
get notes() {
return this.notes_;
}
set notes(v) {
// If the note collection has changed it means the current note might
// have changed or has been deleted, so refresh the note.
this.notes_ = v;
this.reloadNote();
}
get noteId() {
return this.noteId_;
}
set noteId(v) {
this.noteId_ = v;
this.note_ = null;
this.reloadNote();
}
welcomeText() {
return _('Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.');
}
reloadNote() {
if (!this.noteId_ && !this.notes.length) {
this.text = this.welcomeText();
this.scrollTop = 0;
} else if (this.noteId_) {
this.doAsync('loadNote', async () => {
this.note_ = await Note.load(this.noteId_);
if (this.note_ && this.note_.encryption_applied) {
this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.');
} else {
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
}
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
this.lastLoadedNoteId_ = this.noteId_;
});
} else {
this.text = '';
this.scrollTop = 0;
}
}
}
module.exports = NoteWidget;

View File

@@ -0,0 +1,171 @@
const BaseWidget = require('tkwidgets/BaseWidget.js');
const chalk = require('chalk');
const termutils = require('tkwidgets/framework/termutils.js');
const stripAnsi = require('strip-ansi');
const { handleAutocompletion } = require('../autocompletion.js');
class StatusBarWidget extends BaseWidget {
constructor() {
super();
this.promptState_ = null;
this.inputEventEmitter_ = null;
this.history_ = [];
this.items_ = [];
}
get name() {
return 'statusBar';
}
get canHaveFocus() {
return false;
}
setItemAt(index, text) {
this.items_[index] = stripAnsi(text).trim();
this.invalidate();
}
async prompt(initialText = '', promptString = null, options = null) {
if (this.promptState_) throw new Error('Another prompt already active');
if (promptString === null) promptString = ':';
if (options === null) options = {};
this.root.globalDisableKeyboard(this);
this.promptState_ = {
promise: null,
initialText: stripAnsi(initialText),
promptString: stripAnsi(promptString),
};
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
if ('secure' in options) this.promptState_.secure = options.secure;
this.promptState_.promise = new Promise((resolve, reject) => {
this.promptState_.resolve = resolve;
this.promptState_.reject = reject;
});
this.invalidate();
return this.promptState_.promise;
}
get promptActive() {
return !!this.promptState_;
}
get history() {
return this.history_;
}
resetCursor() {
if (!this.promptActive) return;
if (!this.inputEventEmitter_) return;
this.inputEventEmitter_.redraw();
this.inputEventEmitter_.rebase(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString), this.absoluteInnerY);
this.term.moveTo(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString) + this.inputEventEmitter_.getInput().length, this.absoluteInnerY);
}
render() {
super.render();
const doSaveCursor = !this.promptActive;
if (doSaveCursor) this.term.saveCursor();
this.innerClear();
// Note:
// On Ubuntu, bgBlueBright looks too bright which makes the white text illegible
// On Windows, bgBlueBright is fine and looks dark enough (Windows is probably in the wrong though)
// For now, just don't use any colour at all.
//const textStyle = this.promptActive ? (s) => s : chalk.bgBlueBright.white;
//const textStyle = (s) => s;
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_) {
// inputField is already waiting for input so in that case just make
// sure that the cursor is at the right position and exit.
this.resetCursor();
return;
}
this.term.showCursor(true);
const isSecurePrompt = !!this.promptState_.secure;
let options = {
cancelable: true,
history: this.history,
default: this.promptState_.initialText,
autoComplete: handleAutocompletion,
autoCompleteHint : true,
autoCompleteMenu : true,
};
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
if (isSecurePrompt) options.echoChar = true;
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
let resolveResult = null;
const resolveFn = this.promptState_.resolve;
if (error) {
this.logger().error('StatusBar: inputField error:', error);
} else {
if (input === undefined) {
// User cancel
} else {
resolveResult = input ? input.trim() : input;
// Add the command to history but only if it's longer than one character.
// Below that it's usually an answer like "y"/"n", etc.
const isConfigPassword = input.indexOf('config ') >= 0 && input.indexOf('password') >= 0;
if (!isSecurePrompt && input && input.length > 1 && !isConfigPassword) this.history_.push(input);
}
}
// If the inputField spans several lines invalidate the root so that
// the interface is relayouted.
if (termutils.textLength(this.promptState_.promptString) + termutils.textLength(input) >= this.innerWidth - 5) {
this.root.invalidate();
}
this.inputEventEmitter_ = null;
this.term.showCursor(false);
this.promptState_ = null;
this.root.globalEnableKeyboard(this);
this.invalidate();
// 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

@@ -0,0 +1,107 @@
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 rootDir = dirname(dirname(__dirname));
const MAX_WIDTH = 78;
const INDENT = ' ';
function renderTwoColumnData(options, baseIndent, width) {
let output = [];
const optionColWidth = getOptionColWidth(options);
for (let i = 0; i < options.length; i++) {
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");
}
function renderCommandHelp(cmd, width = null) {
if (width === null) width = MAX_WIDTH;
const baseIndent = '';
let output = [];
output.push(baseIndent + cmd.usage());
output.push('');
output.push(wrap(cmd.description(), baseIndent + INDENT, width));
const optionString = renderTwoColumnData(cmd.options(), baseIndent, width);
if (optionString) {
output.push('');
output.push(optionString);
}
if (cmd.name() === 'config') {
const renderMetadata = (md) => {
let desc = [];
if (md.label) {
let label = md.label();
if (label.length && label[label.length - 1] !== '.') label += '.';
desc.push(label);
}
const description = Setting.keyDescription(md.key, 'cli');
if (description) desc.push(description);
desc.push(_('Type: %s.', md.isEnum ? _('Enum') : Setting.typeToString(md.type)));
if (md.isEnum) desc.push(_('Possible values: %s.', Setting.enumOptionsDoc(md.key, '%s (%s)')));
let defaultString = null;
if ('value' in md) {
if (md.type === Setting.TYPE_STRING) {
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');
}
}
if (defaultString !== null) desc.push(_('Default: %s', defaultString));
return [md.key, desc.join("\n")];
};
output.push('');
output.push(_('Possible keys/values:'));
output.push('');
let keysValues = [];
const keys = Setting.keys(true, 'cli');
for (let i = 0; i < keys.length; i++) {
if (keysValues.length) keysValues.push(['','']);
const md = Setting.settingMetadata(keys[i]);
if (!md.label) continue;
keysValues.push(renderMetadata(md));
}
output.push(renderTwoColumnData(keysValues, baseIndent, width));
}
return output.join("\n");
}
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;
}
module.exports = { renderCommandHelp };

View File

@@ -0,0 +1,61 @@
const ArrayUtils = {};
ArrayUtils.unique = function(array) {
return array.filter(function(elem, index, self) {
return index === self.indexOf(elem);
});
}
ArrayUtils.removeElement = function(array, element) {
const index = array.indexOf(element);
if (index < 0) return array;
array.splice(index, 1);
return array;
}
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
ArrayUtils.findByKey = function(array, key, value) {
for (let i = 0; i < array.length; i++) {
const o = array[i];
if (typeof o !== 'object') continue;
if (o[key] === value) return o;
}
return null;
}
ArrayUtils.contentEquals = function(array1, array2) {
if (array1 === array2) return true;
if (!array1.length && !array2.length) return true;
if (array1.length !== array2.length) return false;
for (let i = 0; i < array1.length; i++) {
const a1 = array1[i];
if (array2.indexOf(a1) < 0) return false;
}
return true;
}
module.exports = ArrayUtils;

View File

@@ -0,0 +1,625 @@
const { createStore, applyMiddleware } = require('redux');
const { reducer, defaultState, stateUtils } = 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 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 { splitCommandString } = require('lib/string-utils.js');
const { sprintf } = require('sprintf-js');
const { reg } = require('lib/registry.js');
const { time } = require('lib/time-utils.js');
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const reduxSharedMiddleware = require('lib/components/shared/reduxSharedMiddleware');
const os = require('os');
const fs = require('fs-extra');
const JoplinError = require('lib/JoplinError');
const EventEmitter = require('events');
const syswidecas = require('syswide-cas');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
const EncryptionService = require('lib/services/EncryptionService');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
const RevisionService = require('lib/services/RevisionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseService = require('lib/services/BaseService');
const SearchEngine = require('lib/services/SearchEngine');
const MigrationService = require('lib/services/MigrationService');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
SyncTargetRegistry.addClass(SyncTargetDropbox);
class BaseApplication {
constructor() {
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.eventEmitter_ = new EventEmitter();
// Note: this is basically a cache of state.selectedFolderId. It should *only*
// be derived from the state and not set directly since that would make the
// state and UI out of sync.
this.currentFolder_ = null;
}
logger() {
return this.logger_;
}
store() {
return this.store_;
}
currentFolder() {
return this.currentFolder_;
}
async refreshCurrentFolder() {
let newFolder = null;
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
if (!newFolder) newFolder = await Folder.defaultFolder();
this.switchCurrentFolder(newFolder);
}
switchCurrentFolder(folder) {
if (!this.hasGui()) {
this.currentFolder_ = Object.assign({}, folder);
Setting.setValue('activeFolderId', folder ? folder.id : '');
} else {
this.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : '',
});
}
}
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags_(argv, setDefaults = true) {
let matched = {};
argv = argv.slice(0);
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
while (argv.length) {
let arg = argv[0];
let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError');
matched.profileDir = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--no-welcome') {
matched.welcomeDisabled = true;
argv.splice(0, 1);
continue;
}
if (arg == '--env') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError');
matched.env = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--is-demo') {
Setting.setConstant('isDemo', true);
argv.splice(0, 1);
continue;
}
if (arg == '--open-dev-tools') {
Setting.setConstant('openDevTools', true);
argv.splice(0, 1);
continue;
}
if (arg == '--update-geolocation-disabled') {
Note.updateGeolocationEnabled_ = false;
argv.splice(0, 1);
continue;
}
if (arg == '--stack-trace-enabled') {
this.showStackTraces_ = true;
argv.splice(0, 1);
continue;
}
if (arg == '--log-level') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError');
matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2);
continue;
}
if (arg.indexOf('-psn') === 0) {
// Some weird flag passed by macOS - can be ignored.
// https://github.com/laurent22/joplin/issues/480
// https://stackoverflow.com/questions/10242115
argv.splice(0, 1);
continue;
}
if (arg === '--enable-logging') {
// Electron-specific flag used for debugging - ignore it
argv.splice(0, 1);
continue;
}
if (arg.length && arg[0] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else {
break;
}
}
if (setDefaults) {
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
if (!matched.env) matched.env = 'prod';
}
return {
matched: matched,
argv: argv,
};
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
async exit(code = 0) {
await Setting.saveAll();
process.exit(code);
}
async refreshNotes(state, useSelectedNoteId = false) {
let parentType = state.notesParentType;
let parentId = null;
if (parentType === 'Folder') {
parentId = state.selectedFolderId;
parentType = BaseModel.TYPE_FOLDER;
} else if (parentType === 'Tag') {
parentId = state.selectedTagId;
parentType = BaseModel.TYPE_TAG;
} else if (parentType === 'Search') {
parentId = state.selectedSearchId;
parentType = BaseModel.TYPE_SEARCH;
}
this.logger().debug('Refreshing notes:', parentType, parentId);
let options = {
order: stateUtils.notesOrder(state.settings),
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
showCompletedTodos: Setting.value('showCompletedTodos'),
caseInsensitive: true,
};
const source = JSON.stringify({
options: options,
parentId: parentId,
});
let notes = [];
if (parentId) {
if (parentType === Folder.modelType()) {
notes = await Note.previews(parentId, options);
} else if (parentType === Tag.modelType()) {
notes = await Tag.notes(parentId, options);
} else if (parentType === BaseModel.TYPE_SEARCH) {
const search = BaseModel.byId(state.searches, parentId);
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
}
}
this.store().dispatch({
type: 'NOTE_UPDATE_ALL',
notes: notes,
notesSource: source,
});
if (useSelectedNoteId) {
this.store().dispatch({
type: 'NOTE_SELECT',
id: state.selectedNoteIds && state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
});
} else {
const lastSelectedNoteIds = stateUtils.lastSelectedNoteIds(state);
const foundIds = [];
for (let i = 0; i < lastSelectedNoteIds.length; i++) {
const noteId = lastSelectedNoteIds[i];
let found = false;
for (let j = 0; j < notes.length; j++) {
if (notes[j].id === noteId) {
found = true;
break;
}
}
if (found) foundIds.push(noteId);
}
let selectedNoteId = null;
if (foundIds.length) {
selectedNoteId = foundIds[0];
} else {
selectedNoteId = notes.length ? notes[0].id : null;
}
this.store().dispatch({
type: 'NOTE_SELECT',
id: selectedNoteId,
});
}
}
reducerActionToString(action) {
let o = [action.type];
if ('id' in action) o.push(action.id);
if ('noteId' in action) o.push(action.noteId);
if ('folderId' in action) o.push(action.folderId);
if ('tagId' in action) o.push(action.tagId);
if ('tag' in action) o.push(action.tag.id);
if ('folder' in action) o.push(action.folder.id);
if ('notesSource' in action) o.push(JSON.stringify(action.notesSource));
return o.join(', ');
}
hasGui() {
return false;
}
uiType() {
return this.hasGui() ? 'gui' : 'cli';
}
generalMiddlewareFn() {
const middleware = store => next => (action) => {
return this.generalMiddleware(store, next, action);
}
return middleware;
}
async generalMiddleware(store, next, action) {
this.logger().debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
const newState = store.getState();
let refreshNotes = false;
let refreshFolders = false;
// let refreshTags = false;
let refreshNotesUseSelectedNoteId = false;
await reduxSharedMiddleware(store, next, action);
if (this.hasGui() && ["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(30 * 1000, { syncSteps: ["update_remote", "delete_remote"] });
SearchEngine.instance().scheduleSyncTables();
}
// Don't add FOLDER_UPDATE_ALL as refreshFolders() is calling it too, which
// would cause the sidebar to refresh all the time.
if (this.hasGui() && ["FOLDER_UPDATE_ONE"].indexOf(action.type) >= 0) {
refreshFolders = true;
}
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || action.type === 'FOLDER_AND_NOTE_SELECT' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
refreshNotes = true;
if (action.type === 'FOLDER_AND_NOTE_SELECT') refreshNotesUseSelectedNoteId = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'uncompletedTodosOnTop') || action.type == 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showCompletedTodos') || action.type == 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
refreshNotes = true;
}
if (action.type == 'SEARCH_SELECT' || action.type === 'SEARCH_DELETE') {
refreshNotes = true;
}
// if (action.type == 'NOTE_DELETE') {
// refreshTags = true;
// }
if (refreshNotes) {
await this.refreshNotes(newState, refreshNotesUseSelectedNoteId);
}
// if (refreshTags) {
// this.dispatch({
// type: 'TAG_UPDATE_ALL',
// items: await Tag.allWithNotes(),
// });
// }
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || (action.type == 'SETTING_UPDATE_ALL')) {
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));
}
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.ignoreTlsErrors') || (action.type == 'SETTING_UPDATE_ALL')) {
// https://stackoverflow.com/questions/20082893/unable-to-verify-leaf-signature
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = Setting.value('net.ignoreTlsErrors') ? '0' : '1';
}
if ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'net.customCertificates') || (action.type == 'SETTING_UPDATE_ALL')) {
const caPaths = Setting.value('net.customCertificates').split(',');
for (let i = 0; i < caPaths.length; i++) {
const f = caPaths[i].trim();
if (!f) continue;
syswidecas.addCAs(f);
}
}
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) {
if (this.hasGui()) {
await EncryptionService.instance().loadMasterKeysFromSettings();
DecryptionWorker.instance().scheduleStart();
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();
this.dispatch({
type: 'MASTERKEY_REMOVE_NOT_LOADED',
ids: loadedMasterKeyIds,
});
// Schedule a sync operation so that items that need to be encrypted
// are sent to sync target.
reg.scheduleSync();
}
}
if (action.type === 'NOTE_UPDATE_ONE') {
refreshFolders = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('folders.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
refreshFolders = 'now';
}
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}
if (this.hasGui() && action.type === 'SYNC_GOT_ENCRYPTED_ITEM') {
DecryptionWorker.instance().scheduleStart();
}
if (this.hasGui() && action.type === 'SYNC_CREATED_RESOURCE') {
ResourceFetcher.instance().queueDownload(action.id);
}
if (refreshFolders) {
if (refreshFolders === 'now') {
await FoldersScreenUtils.refreshFolders();
} else {
await FoldersScreenUtils.scheduleRefreshFolders();
}
}
return result;
}
dispatch(action) {
if (this.store()) return this.store().dispatch(action);
}
reducer(state = defaultState, action) {
return reducer(state, action);
}
initRedux() {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn()));
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
reg.dispatch = this.store().dispatch;
BaseSyncTarget.dispatch = this.store().dispatch;
DecryptionWorker.instance().dispatch = this.store().dispatch;
ResourceFetcher.instance().dispatch = this.store().dispatch;
}
async readFlagsFromFile(flagPath) {
if (!fs.existsSync(flagPath)) return {};
let flagContent = fs.readFileSync(flagPath, 'utf8');
if (!flagContent) return {};
flagContent = flagContent.trim();
let flags = splitCommandString(flagContent);
flags.splice(0, 0, 'cmd');
flags.splice(0, 0, 'node');
flags = await this.handleStartFlags_(flags, false);
return flags.matched;
}
determineProfileDir(initArgs) {
if (initArgs.profileDir) return initArgs.profileDir;
if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) return process.env.PORTABLE_EXECUTABLE_DIR + '/JoplinProfile';
return os.homedir() + '/.config/' + Setting.value('appName');
}
async testing() {
const markdownUtils = require('lib/markdownUtils');
const ClipperServer = require('lib/ClipperServer');
const server = new ClipperServer();
const HtmlToMd = require('lib/HtmlToMd');
const service = new HtmlToMd();
const html = await shim.fsDriver().readFile('/mnt/d/test.html');
let markdown = service.parse(html, { baseUrl: 'https://duckduckgo.com/' });
console.info(markdown);
console.info('--------------------------------------------------');
const imageUrls = markdownUtils.extractImageUrls(markdown);
let result = await server.downloadImages_(imageUrls);
result = await server.createResourcesFromPaths_(result);
console.info(result);
markdown = server.replaceImageUrlsByResources_(markdown, result);
console.info('--------------------------------------------------');
console.info(markdown);
console.info('--------------------------------------------------');
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);
argv = startFlags.argv;
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
let appName = initArgs.env == 'dev' ? 'joplindev' : 'joplin';
if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop';
Setting.setConstant('appName', appName);
const profileDir = this.determineProfileDir(initArgs);
const resourceDirName = 'resources';
const resourceDir = profileDir + '/' + resourceDirName;
const tempDir = profileDir + '/tmp';
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDirName', resourceDirName);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
await shim.fsDriver().remove(tempDir);
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
// Clean up any remaining watched files (they start with "edit-")
await shim.fsDriver().removeAllThatStartWith(profileDir, 'edit-');
const extraFlags = await this.readFlagsFromFile(profileDir + '/flags.txt');
initArgs = Object.assign(initArgs, extraFlags);
this.logger_.addTarget('file', { path: profileDir + '/log.txt' });
if (Setting.value('env') === 'dev') this.logger_.addTarget('console', { level: Logger.LEVEL_WARN });
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
reg.dispatch = (o) => {};
this.dbLogger_.addTarget('file', { path: profileDir + '/log-database.txt' });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_INFO);
}
this.logger_.info('Profile directory: ' + profileDir);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
await this.database_.open({ name: profileDir + '/database.sqlite' });
reg.setDb(this.database_);
BaseModel.db_ = this.database_;
await Setting.load();
if (Setting.value('firstStart')) {
const locale = shim.detectAndSetLocale(Setting);
reg.logger().info('First start: detected locale as ' + locale);
if (Setting.value('env') === 'dev') {
Setting.setValue('showTrayIcon', 0);
Setting.setValue('autoUpdateEnabled', 0);
Setting.setValue('sync.interval', 3600);
}
Setting.setValue('firstStart', 0);
} else {
setLocale(Setting.value('locale'));
}
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
if (!Setting.value('api.token')) {
EncryptionService.instance().randomHexString(64).then((token) => {
Setting.setValue('api.token', token);
});
}
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));
BaseItem.revisionService_ = RevisionService.instance();
BaseService.logger_ = this.logger_;
EncryptionService.instance().setLogger(this.logger_);
BaseItem.encryptionService_ = EncryptionService.instance();
DecryptionWorker.instance().setLogger(this.logger_);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
await EncryptionService.instance().loadMasterKeysFromSettings();
ResourceFetcher.instance().setFileApi(() => { return reg.syncTarget().fileApi() });
ResourceFetcher.instance().setLogger(this.logger_);
ResourceFetcher.instance().start();
SearchEngine.instance().setDb(reg.db());
SearchEngine.instance().setLogger(reg.logger());
SearchEngine.instance().scheduleSyncTables();
let currentFolderId = Setting.value('activeFolderId');
let currentFolder = null;
if (currentFolderId) currentFolder = await Folder.load(currentFolderId);
if (!currentFolder) currentFolder = await Folder.defaultFolder();
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
await MigrationService.instance().run();
return argv;
}
}
module.exports = { BaseApplication };

View File

@@ -0,0 +1,555 @@
const { Database } = require('lib/database.js');
const { uuid } = require('lib/uuid.js');
const { time } = require('lib/time-utils.js');
const Mutex = require('async-mutex').Mutex;
class BaseModel {
static modelType() {
throw new Error('Must be overriden');
}
static tableName() {
throw new Error('Must be overriden');
}
static addModelMd(model) {
if (!model) return model;
if (Array.isArray(model)) {
let output = [];
for (let i = 0; i < model.length; i++) {
output.push(this.addModelMd(model[i]));
}
return output;
} else {
model = Object.assign({}, model);
model.type_ = this.modelType();
return model;
}
}
static logger() {
return this.db().logger();
}
static useUuid() {
return false;
}
static byId(items, id) {
for (let i = 0; i < items.length; i++) {
if (items[i].id == id) return items[i];
}
return null;
}
static modelIndexById(items, id) {
for (let i = 0; i < items.length; i++) {
if (items[i].id == id) return i;
}
return -1;
}
static modelsByIds(items, ids) {
const output = [];
for (let i = 0; i < items.length; i++) {
if (ids.indexOf(items[i].id) >= 0) {
output.push(items[i]);
}
}
return output;
}
// Prefer the use of this function to compare IDs as it handles the case where
// one ID is null and the other is "", in which case they are actually considered to be the same.
static idsEqual(id1, id2) {
if (!id1 && !id2) return true;
if (!id1 && !!id2) return false;
if (!!id1 && !id2) return false;
return id1 === id2;
}
static modelTypeToName(type) {
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i];
if (e[1] === type) return e[0].substr(5).toLowerCase();
}
throw new Error('Unknown model type: ' + type);
}
static hasField(name) {
let fields = this.fieldNames();
return fields.indexOf(name) >= 0;
}
static fieldNames(withPrefix = false) {
let output = this.db().tableFieldNames(this.tableName());
if (!withPrefix) return output;
let p = withPrefix === true ? this.tableName() : withPrefix;
let temp = [];
for (let i = 0; i < output.length; i++) {
temp.push(p + '.' + output[i]);
}
return temp;
}
static fieldType(name, defaultValue = null) {
let fields = this.fields();
for (let i = 0; i < fields.length; i++) {
if (fields[i].name == name) return fields[i].type;
}
if (defaultValue !== null) return defaultValue;
throw new Error('Unknown field: ' + name);
}
static fields() {
return this.db().tableFields(this.tableName());
}
static removeUnknownFields(model) {
const newModel = {};
for (let n in model) {
if (!model.hasOwnProperty(n)) continue;
if (!this.hasField(n) && n !== 'type_') continue;
newModel[n] = model[n];
}
return newModel;
}
static new() {
let fields = this.fields();
let output = {};
for (let i = 0; i < fields.length; i++) {
let f = fields[i];
output[f.name] = f.default;
}
return output;
}
static modOptions(options) {
if (!options) {
options = {};
} else {
options = Object.assign({}, options);
}
if (!('isNew' in options)) options.isNew = 'auto';
if (!('autoTimestamp' in options)) options.autoTimestamp = true;
return options;
}
static count(options = null) {
if (!options) options = {};
let sql = 'SELECT count(*) as total FROM `' + this.tableName() + '`';
if (options.where) sql += ' WHERE ' + options.where;
return this.db().selectOne(sql).then((r) => {
return r ? r['total'] : 0;
});
}
static load(id) {
return this.loadByField('id', id);
}
static shortId(id) {
return id.substr(0, 5);
}
static loadByPartialId(partialId) {
return this.modelSelectAll('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']);
}
static applySqlOptions(options, sql, params = null) {
if (!options) options = {};
if (options.order && options.order.length) {
let items = [];
for (let i = 0; i < options.order.length; i++) {
const o = options.order[i];
let item = o.by;
if (options.caseInsensitive === true) item += ' COLLATE NOCASE';
if (o.dir) item += ' ' + o.dir;
items.push(item);
}
sql += ' ORDER BY ' + items.join(', ');
}
if (options.limit) sql += ' LIMIT ' + options.limit;
return { sql: sql, params: params };
}
static async allIds(options = null) {
let q = this.applySqlOptions(options, 'SELECT id FROM `' + this.tableName() + '`');
const rows = await this.db().selectAll(q.sql, q.params);
return rows.map((r) => r.id);
}
static async all(options = null) {
if (!options) options = {};
if (!options.fields) options.fields = '*';
let q = this.applySqlOptions(options, 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`');
return this.modelSelectAll(q.sql);
}
static async byIds(ids, options = null) {
if (!ids.length) return [];
if (!options) options = {};
if (!options.fields) options.fields = '*';
let sql = 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`';
sql += ' WHERE id IN ("' + ids.join('","') + '")';
let q = this.applySqlOptions(options, sql);
return this.modelSelectAll(q.sql);
}
static async search(options = null) {
if (!options) options = {};
if (!options.fields) options.fields = '*';
let conditions = options.conditions ? options.conditions.slice(0) : [];
let params = options.conditionsParams ? options.conditionsParams.slice(0) : [];
if (options.titlePattern) {
let pattern = options.titlePattern.replace(/\*/g, '%');
conditions.push('title LIKE ?');
params.push(pattern);
}
if ('limit' in options && options.limit <= 0) return [];
let sql = 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`';
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
let query = this.applySqlOptions(options, sql, params);
return this.modelSelectAll(query.sql, query.params);
}
static modelSelectOne(sql, params = null) {
if (params === null) params = [];
return this.db().selectOne(sql, params).then((model) => {
return this.filter(this.addModelMd(model));
});
}
static modelSelectAll(sql, params = null) {
if (params === null) params = [];
return this.db().selectAll(sql, params).then((models) => {
return this.filterArray(this.addModelMd(models));
});
}
static loadByField(fieldName, fieldValue, options = null) {
if (!options) options = {};
if (!('caseInsensitive' in options)) options.caseInsensitive = false;
let sql = 'SELECT * FROM `' + this.tableName() + '` WHERE `' + fieldName + '` = ?';
if (options.caseInsensitive) sql += ' COLLATE NOCASE';
return this.modelSelectOne(sql, [fieldValue]);
}
static loadByTitle(fieldValue) {
return this.modelSelectOne('SELECT * FROM `' + this.tableName() + '` WHERE `title` = ?', [fieldValue]);
}
static diffObjects(oldModel, newModel) {
let output = {};
const fields = this.diffObjectsFields(oldModel, newModel);
for (let i = 0; i < fields.length; i++) {
output[fields[i]] = newModel[fields[i]];
}
if ('type_' in newModel) output.type_ = newModel.type_;
return output;
}
static diffObjectsFields(oldModel, newModel) {
let output = [];
for (let n in newModel) {
if (!newModel.hasOwnProperty(n)) continue;
if (n == 'type_') continue;
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
output.push(n);
}
}
return output;
}
static modelsAreSame(oldModel, newModel) {
const diff = this.diffObjects(oldModel, newModel);
delete diff.type_;
return !Object.getOwnPropertyNames(diff).length;
}
static saveMutex(modelOrId) {
const noLockMutex = {
acquire: function() { return null; }
};
if (!modelOrId) return noLockMutex;
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
if (!modelId) return noLockMutex;
let mutex = BaseModel.saveMutexes_[modelId];
if (mutex) return mutex;
mutex = new Mutex();
BaseModel.saveMutexes_[modelId] = mutex;
return mutex;
}
static releaseSaveMutex(modelOrId, release) {
if (!release) return;
if (!modelOrId) return release();
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
if (!modelId) return release();
let mutex = BaseModel.saveMutexes_[modelId];
if (!mutex) return release();
delete BaseModel.saveMutexes_[modelId];
release();
}
static saveQuery(o, options) {
let temp = {}
let fieldNames = this.fieldNames();
for (let i = 0; i < fieldNames.length; i++) {
let n = fieldNames[i];
if (n in o) temp[n] = o[n];
}
// Remove fields that are not in the `fields` list, if provided.
// Note that things like update_time, user_updated_time will still
// be part of the final list of fields if autoTimestamp is on.
// id also will stay.
if (!options.isNew && options.fields) {
const filtered = {};
for (let k in temp) {
if (!temp.hasOwnProperty(k)) continue;
if (k !== 'id' && options.fields.indexOf(k) < 0) continue;
filtered[k] = temp[k];
}
temp = filtered;
}
o = temp;
let modelId = temp.id;
let query = {};
const timeNow = time.unixMs();
if (options.autoTimestamp && this.hasField('updated_time')) {
o.updated_time = timeNow;
}
// The purpose of user_updated_time is to allow the user to manually set the time of a note (in which case
// options.autoTimestamp will be `false`). However note that if the item is later changed, this timestamp
// will be set again to the current time.
//
// The technique to modify user_updated_time while keeping updated_time current (so that sync can happen) is to
// manually set updated_time when saving and to set autoTimestamp to false, for example:
// Note.save({ id: "...", updated_time: Date.now(), user_updated_time: 1436342618000 }, { autoTimestamp: false })
if (options.autoTimestamp && this.hasField('user_updated_time')) {
o.user_updated_time = timeNow;
}
if (options.isNew) {
if (this.useUuid() && !o.id) {
modelId = uuid.create();
o.id = modelId;
}
if (!o.created_time && this.hasField('created_time')) {
o.created_time = timeNow;
}
if (!o.user_created_time && this.hasField('user_created_time')) {
o.user_created_time = o.created_time ? o.created_time : timeNow;
}
if (!o.user_updated_time && this.hasField('user_updated_time')) {
o.user_updated_time = o.updated_time ? o.updated_time : timeNow;
}
query = Database.insertQuery(this.tableName(), o);
} else {
let where = { id: o.id };
let temp = Object.assign({}, o);
delete temp.id;
query = Database.updateQuery(this.tableName(), temp, where);
}
query.id = modelId;
query.modObject = o;
return query;
}
static async save(o, options = null) {
// When saving, there's a mutex per model ID. This is because the model returned from this function
// is basically its input `o` (instead of being read from the database, for performance reasons).
// This works well in general except if that model is saved simultaneously in two places. In that
// case, the output won't be up-to-date and would cause for example display issues with out-dated
// notes being displayed. This was an issue when notes were being synchronised while being decrypted
// at the same time.
const mutexRelease = await this.saveMutex(o).acquire();
options = this.modOptions(options);
options.isNew = this.isNew(o, options);
// Diff saving is an optimisation which takes a new version of the item and an old one,
// do a diff and save only this diff. IMPORTANT: When using this make sure that both
// models have been normalised using ItemClass.filter()
const isDiffSaving = options && options.oldItem && !options.isNew;
if (isDiffSaving) {
const newObject = BaseModel.diffObjects(options.oldItem, o);
newObject.type_ = o.type_;
newObject.id = o.id;
o = newObject;
}
o = this.filter(o);
let queries = [];
let saveQuery = this.saveQuery(o, options);
let modelId = saveQuery.id;
queries.push(saveQuery);
if (options.nextQueries && options.nextQueries.length) {
queries = queries.concat(options.nextQueries);
}
let output = null;
try {
await this.db().transactionExecBatch(queries);
o = Object.assign({}, o);
if (modelId) o.id = modelId;
if ('updated_time' in saveQuery.modObject) o.updated_time = saveQuery.modObject.updated_time;
if ('created_time' in saveQuery.modObject) o.created_time = saveQuery.modObject.created_time;
if ('user_updated_time' in saveQuery.modObject) o.user_updated_time = saveQuery.modObject.user_updated_time;
if ('user_created_time' in saveQuery.modObject) o.user_created_time = saveQuery.modObject.user_created_time;
o = this.addModelMd(o);
if (isDiffSaving) {
for (let n in options.oldItem) {
if (!options.oldItem.hasOwnProperty(n)) continue;
if (n in o) continue;
o[n] = options.oldItem[n];
}
}
output = this.filter(o);
} finally {
this.releaseSaveMutex(o, mutexRelease);
}
return output;
}
static isNew(object, options) {
if (options && ('isNew' in options)) {
// options.isNew can be "auto" too
if (options.isNew === true) return true;
if (options.isNew === false) return false;
}
return !object.id;
}
static filterArray(models) {
let output = [];
for (let i = 0; i < models.length; i++) {
output.push(this.filter(models[i]));
}
return output;
}
static filter(model) {
if (!model) return model;
let output = Object.assign({}, model);
for (let n in output) {
if (!output.hasOwnProperty(n)) continue;
// The SQLite database doesn't have booleans so cast everything to int
if (output[n] === true) {
output[n] = 1;
} else if (output[n] === false) {
output[n] = 0;
} else {
const t = this.fieldType(n, Database.TYPE_UNKNOWN);
if (t === Database.TYPE_INT) {
output[n] = !n ? 0 : parseInt(output[n], 10);
}
}
}
return output;
}
static delete(id, options = null) {
if (!id) throw new Error('Cannot delete object without an ID');
options = this.modOptions(options);
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]);
}
static batchDelete(ids, options = null) {
if (!ids.length) return;
options = this.modOptions(options);
const idFieldName = options.idFieldName ? options.idFieldName : 'id';
const sql = 'DELETE FROM ' + this.tableName() + ' WHERE ' + idFieldName + ' IN ("' + ids.join('","') + '")';
return this.db().exec(sql);
}
static db() {
if (!this.db_) throw new Error('Accessing database before it has been initialised');
return this.db_;
}
static isReady() {
return !!this.db_;
}
}
BaseModel.typeEnum_ = [
['TYPE_NOTE', 1],
['TYPE_FOLDER', 2],
['TYPE_SETTING', 3],
['TYPE_RESOURCE', 4],
['TYPE_TAG', 5],
['TYPE_NOTE_TAG', 6],
['TYPE_SEARCH', 7],
['TYPE_ALARM', 8],
['TYPE_MASTER_KEY', 9],
['TYPE_ITEM_CHANGE', 10],
['TYPE_NOTE_RESOURCE', 11],
['TYPE_RESOURCE_LOCAL_STATE', 12],
['TYPE_REVISION', 13],
['TYPE_MIGRATION', 14],
];
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i];
BaseModel[e[0]] = e[1];
}
BaseModel.db_ = null;
BaseModel.dispatch = function(o) {};
BaseModel.saveMutexes_ = {};
module.exports = BaseModel;

View File

@@ -0,0 +1,129 @@
const EncryptionService = require('lib/services/EncryptionService.js');
class BaseSyncTarget {
constructor(db, options = null) {
this.db_ = db;
this.synchronizer_ = null;
this.initState_ = null;
this.logger_ = null;
this.options_ = options;
}
static supportsConfigCheck() {
return false;
}
static resourceDirName() {
return '.resource';
}
option(name, defaultValue = null) {
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
}
logger() {
return this.logger_;
}
setLogger(v) {
this.logger_ = v;
}
db() {
return this.db_;
}
async isAuthenticated() {
return false;
}
authRouteName() {
return null;
}
static id() {
throw new Error('id() not implemented');
}
// Note: it cannot be called just "name()" because that's a reserved keyword and
// it would throw an obscure error in React Native.
static targetName() {
throw new Error('targetName() not implemented');
}
static label() {
throw new Error('label() not implemented');
}
async initSynchronizer() {
throw new Error('initSynchronizer() not implemented');
}
async initFileApi() {
throw new Error('initFileApi() not implemented');
}
async fileApi() {
if (this.fileApi_) return this.fileApi_;
this.fileApi_ = await this.initFileApi();
return this.fileApi_;
}
fileApiSync() {
return this.fileApi_;
}
// Usually each sync target should create and setup its own file API via initFileApi()
// but for testing purposes it might be convenient to provide it here so that multiple
// clients can share and sync to the same file api (see test-utils.js)
setFileApi(v) {
this.fileApi_ = v;
}
async synchronizer() {
if (this.synchronizer_) return this.synchronizer_;
if (this.initState_ == 'started') {
// Synchronizer is already being initialized, so wait here till it's done.
return new Promise((resolve, reject) => {
const iid = setInterval(() => {
if (this.initState_ == 'ready') {
clearInterval(iid);
resolve(this.synchronizer_);
}
if (this.initState_ == 'error') {
clearInterval(iid);
reject(new Error('Could not initialise synchroniser'));
}
}, 1000);
});
} else {
this.initState_ = 'started';
try {
this.synchronizer_ = await this.initSynchronizer();
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.setEncryptionService(EncryptionService.instance());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;
} catch (error) {
this.initState_ = 'error';
throw error;
}
}
}
async syncStarted() {
if (!this.synchronizer_) return false;
if (!await this.isAuthenticated()) return false;
const sync = await this.synchronizer();
return sync.state() != 'idle';
}
}
BaseSyncTarget.dispatch = (action) => {};
module.exports = BaseSyncTarget;

View File

@@ -0,0 +1,35 @@
class Cache {
async getItem(name) {
let output = null;
try {
const storage = await Cache.storage();
output = await storage.getItem(name);
} catch (error) {
console.info(error);
// Defaults to returning null
}
return output;
}
async setItem(name, value, ttl = null) {
try {
const storage = await Cache.storage();
const options = {};
if (ttl !== null) options.ttl = ttl;
await storage.setItem(name, value, options);
} catch (error) {
// Defaults to not saving to cache
}
}
}
Cache.storage = async function() {
if (Cache.storage_) return Cache.storage_;
Cache.storage_ = require('node-persist');
await Cache.storage_.init({ dir: require('os').tmpdir() + '/joplin-cache', ttl: 1000 * 60 });
return Cache.storage_;
}
module.exports = Cache;

View File

@@ -0,0 +1,217 @@
const { netUtils } = require('lib/net-utils');
const urlParser = require("url");
const Setting = require('lib/models/Setting');
const { Logger } = require('lib/logger.js');
const randomClipperPort = require('lib/randomClipperPort');
const enableServerDestroy = require('server-destroy');
const Api = require('lib/services/rest/Api');
const ApiResponse = require('lib/services/rest/ApiResponse');
const multiparty = require('multiparty');
class ClipperServer {
constructor() {
this.logger_ = new Logger();
this.startState_ = 'idle';
this.server_ = null;
this.port_ = null;
this.api_ = new Api(() => {
return Setting.value('api.token');
});
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ClipperServer();
return this.instance_;
}
setLogger(l) {
this.logger_ = l;
this.api_.setLogger(l);
}
logger() {
return this.logger_;
}
setDispatch(d) {
this.dispatch_ = d;
}
dispatch(action) {
if (!this.dispatch_) throw new Error('dispatch not set!');
this.dispatch_(action);
}
setStartState(v) {
if (this.startState_ === v) return;
this.startState_ = v;
this.dispatch({
type: 'CLIPPER_SERVER_SET',
startState: v,
});
}
setPort(v) {
if (this.port_ === v) return;
this.port_ = v;
this.dispatch({
type: 'CLIPPER_SERVER_SET',
port: v,
});
}
async findAvailablePort() {
const tcpPortUsed = require('tcp-port-used');
let state = null;
for (let i = 0; i < 10000; i++) {
state = randomClipperPort(state, Setting.value('env'));
const inUse = await tcpPortUsed.check(state.port);
if (!inUse) return state.port;
}
throw new Error('All potential ports are in use or not available.')
}
async start() {
this.setPort(null);
this.setStartState('starting');
try {
const p = await this.findAvailablePort();
this.setPort(p);
} catch (error) {
this.setStartState('idle');
this.logger().error(error);
return;
}
this.server_ = require('http').createServer();
this.server_.on('request', async (request, response) => {
const writeCorsHeaders = (code, contentType = "application/json", additionalHeaders = null) => {
const headers = Object.assign({}, {
"Content-Type": contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
}, additionalHeaders ? additionalHeaders : {});
response.writeHead(code, headers);
}
const writeResponseJson = (code, object) => {
writeCorsHeaders(code);
response.write(JSON.stringify(object));
response.end();
}
const writeResponseText = (code, text) => {
writeCorsHeaders(code, 'text/plain');
response.write(text);
response.end();
}
const writeResponseInstance = (code, instance) => {
if (instance.type === 'attachment') {
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
'Content-disposition': 'attachment; filename=' + filename,
'Content-Length': instance.body.length,
});
response.end(instance.body);
} else {
throw new Error('Not implemented');
}
}
const writeResponse = (code, response) => {
if (response instanceof ApiResponse) {
writeResponseInstance(code, response);
} else if (typeof response === 'string') {
writeResponseText(code, response);
} else {
writeResponseJson(code, response);
}
}
this.logger().info('Request: ' + request.method + ' ' + request.url);
const url = urlParser.parse(request.url, true);
const execRequest = async (request, body = '', files = []) => {
try {
const response = await this.api_.route(request.method, url.pathname, url.query, body, files);
writeResponse(200, response ? response : '');
} catch (error) {
this.logger().error(error);
const httpCode = error.httpCode ? error.httpCode : 500;
const msg = [];
if (httpCode >= 500) msg.push('Internal Server Error');
if (error.message) msg.push(error.message);
writeResponse(httpCode, { error: msg.join(': ') });
}
}
const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
if (request.method === 'OPTIONS') {
writeCorsHeaders(200);
response.end();
} else {
if (contentType.indexOf('multipart/form-data') === 0) {
const form = new multiparty.Form();
form.parse(request, function(error, fields, files) {
if (error) {
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
return;
} else {
execRequest(
request,
fields && fields.props && fields.props.length ? fields.props[0] : '',
files && files.data ? files.data : []
);
}
});
} else {
if (request.method === 'POST' || request.method === 'PUT') {
let body = '';
request.on('data', (data) => {
body += data;
});
request.on('end', async () => {
execRequest(request, body);
});
} else {
execRequest(request);
}
}
}
});
enableServerDestroy(this.server_);
this.logger().info('Starting Clipper server on port ' + this.port_);
this.server_.listen(this.port_, '127.0.0.1');
this.setStartState('started');
}
async stop() {
this.server_.destroy();
this.server_ = null;
this.setStartState('idle');
this.setPort(null);
}
}
module.exports = ClipperServer;

View File

@@ -0,0 +1,216 @@
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim.js');
const JoplinError = require('lib/JoplinError');
const URL = require('url-parse');
const { time } = require('lib/time-utils');
const EventDispatcher = require('lib/EventDispatcher');
class DropboxApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
this.authToken_ = null;
this.dispatcher_ = new EventDispatcher();
}
clientId() {
return this.options_.id;
}
clientSecret() {
return this.options_.secret;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
authToken() {
return this.authToken_; // Without the "Bearer " prefix
}
on(eventName, callback) {
return this.dispatcher_.on(eventName, callback);
}
setAuthToken(v) {
this.authToken_ = v;
this.dispatcher_.dispatch('authRefreshed', this.authToken());
}
loginUrl() {
return 'https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=' + this.clientId();
}
baseUrl(endPointFormat) {
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error('Invalid end point format: ' + endPointFormat);
return 'https://' + endPointFormat + '.dropboxapi.com/2';
}
requestToCurl_(url, options) {
let output = [];
output.push('curl');
if (options.method) output.push('-X ' + options.method);
if (options.headers) {
for (let n in options.headers) {
if (!options.headers.hasOwnProperty(n)) continue;
output.push('-H ' + "'" + n + ': ' + options.headers[n] + "'");
}
}
if (options.body) output.push('--data ' + '"' + options.body + '"');
output.push(url);
return output.join(' ');
}
async execAuthToken(authCode) {
const postData = {
code: authCode,
grant_type: 'authorization_code',
client_id: this.clientId(),
client_secret: this.clientSecret(),
};
var formBody = [];
for (var property in postData) {
var encodedKey = encodeURIComponent(property);
var encodedValue = encodeURIComponent(postData[property]);
formBody.push(encodedKey + "=" + encodedValue);
}
formBody = formBody.join("&");
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: formBody
});
const responseText = await response.text();
if (!response.ok) throw new Error(responseText);
return JSON.parse(responseText);
}
isTokenError(status, responseText) {
if (status === 401) return true;
if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
// eg. Error: POST files/create_folder_v2: Error (400): Error in call to API function "files/create_folder_v2": Must provide HTTP header "Authorization" or URL parameter "authorization".
if (responseText.indexOf('Must provide HTTP header "Authorization"') >= 0) return true;
return false;
}
async exec(method, path = '', body = null, headers = null, options = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.target) options.target = 'string';
const authToken = this.authToken();
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';
if (endPointFormat === 'api') {
headers['Content-Type'] = 'application/json';
if (body && typeof body === 'object') body = JSON.stringify(body);
} else {
headers['Content-Type'] = 'application/octet-stream';
}
const fetchOptions = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
const url = path.indexOf('https://') === 0 ? path : this.baseUrl(endPointFormat) + '/' + path;
let tryCount = 0;
while (true) {
try {
let response = null;
// console.info(this.requestToCurl_(url, fetchOptions));
// console.info(method + ' ' + url);
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
response = await shim.fetch(url, fetchOptions);
} else { // file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// console.info('Response: ' + responseText);
let responseJson_ = null;
const loadResponseJson = () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
try {
responseJson_ = JSON.parse(responseText);
} catch (error) {
return { error: responseText };
}
return responseJson_;
}
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message) => {
const json = loadResponseJson();
let code = '';
if (json && json.error_summary) {
code = json.error_summary;
}
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (responseText + '').substr(0, 1024);
const error = new JoplinError(method + ' ' + path + ': ' + message + ' (' + response.status + '): ' + shortResponseText, code);
error.httpStatus = response.status;
return error;
}
if (!response.ok) {
const json = loadResponseJson();
if (this.isTokenError(response.status, responseText)) {
this.setAuthToken(null);
}
// When using fetchBlob we only get a string (not xml or json) back
if (options.target === 'file') throw newError('fetchBlob error');
throw newError('Error');
}
if (options.responseFormat === 'text') return responseText;
return loadResponseJson();
} catch (error) {
tryCount++;
if (error && typeof error.code === 'string' && error.code.indexOf('too_many_write_operations') >= 0) {
this.logger().warn('too_many_write_operations ' + tryCount);
if (tryCount >= 3) {
throw error;
}
await time.sleep(tryCount * 2);
} else {
throw error;
}
}
}
}
}
module.exports = DropboxApi;

View File

@@ -32,4 +32,4 @@ class EventDispatcher {
}
module.exports = { EventDispatcher };
module.exports = EventDispatcher;

View File

@@ -0,0 +1,21 @@
const TurndownService = require('joplin-turndown')
const markdownUtils = require('lib/markdownUtils');
class HtmlToMd {
parse(html, options = {}) {
const turndownPluginGfm = require('joplin-turndown-plugin-gfm').gfm
const turndown = new TurndownService({
headingStyle: 'atx',
})
turndown.use(turndownPluginGfm)
turndown.remove('script');
turndown.remove('style');
let md = turndown.turndown(html)
if (options.baseUrl) md = markdownUtils.prependBaseUrl(md, options.baseUrl);
return md;
}
}
module.exports = HtmlToMd;

View File

@@ -0,0 +1,10 @@
class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code = code;
}
}
module.exports = JoplinError;

View File

@@ -0,0 +1,182 @@
const MarkdownIt = require('markdown-it');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const { shim } = require('lib/shim.js');
const { _ } = require('lib/locale');
const md5 = require('md5');
const StringUtils = require('lib/string-utils.js');
const noteStyle = require('./MdToHtml/noteStyle');
const Setting = require('./models/Setting.js');
const rules = {
image: require('./MdToHtml/rules/image'),
checkbox: require('./MdToHtml/rules/checkbox'),
katex: require('./MdToHtml/rules/katex'),
link_open: require('./MdToHtml/rules/link_open'),
html_image: require('./MdToHtml/rules/html_image'),
highlight_keywords: require('./MdToHtml/rules/highlight_keywords'),
code_inline: require('./MdToHtml/rules/code_inline'),
};
const setupLinkify = require('./MdToHtml/setupLinkify');
const hljs = require('highlight.js');
const markdownItAnchor = require('markdown-it-anchor');
const markdownItToc = require('markdown-it-toc-done-right');
// The keys must match the corresponding entry in Setting.js
const plugins = {
mark: require('markdown-it-mark'),
footnote: require('markdown-it-footnote'),
sub: require('markdown-it-sub'),
sup: require('markdown-it-sup'),
deflist: require('markdown-it-deflist'),
abbr: require('markdown-it-abbr'),
emoji: require('markdown-it-emoji'),
insert: require('markdown-it-ins'),
multitable: require('markdown-it-multimd-table'),
};
class MdToHtml {
constructor(options = null) {
if (!options) options = {};
// Must include last "/"
this.resourceBaseUrl_ = ('resourceBaseUrl' in options) ? options.resourceBaseUrl : null;
this.cachedOutputs_ = {};
}
render(body, style, options = null) {
if (!options) options = {};
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
if (!options.paddingBottom) options.paddingBottom = '0';
if (!options.highlightedKeywords) options.highlightedKeywords = [];
const breaks_ = Setting.value('markdown.softbreaks') ? false : true;
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style)));
const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput;
const context = {
css: {},
cssFiles: {},
assetLoaders: {},
};
const markdownIt = new MarkdownIt({
breaks: breaks_,
linkify: true,
html: true,
highlight: function(str, lang) {
try {
let hlCode = '';
if (lang && hljs.getLanguage(lang)) {
hlCode = hljs.highlight(lang, str, true).value;
} else {
hlCode = hljs.highlightAuto(str).value;
}
if (shim.isReactNative()) {
context.css['hljs'] = shim.loadCssFromJs(options.codeTheme);
} else {
context.cssFiles['hljs'] = 'highlight/styles/' + options.codeTheme;
}
return '<pre class="hljs"><code>' + hlCode + '</code></pre>';
} catch (error) {
return '<pre class="hljs"><code>' + markdownIt.utils.escapeHtml(str) + '</code></pre>';
}
}
});
const ruleOptions = Object.assign({}, options, { resourceBaseUrl: this.resourceBaseUrl_ });
// To add a plugin, there are three options:
//
// 1. If the plugin does not need any application specific data, use the standard way:
//
// const someMarkdownPlugin = require('someMarkdownPlugin');
// markdownIt.use(someMarkdownPlugin);
//
// 2. If the plugin does not need any application specific data, and you want the user
// to be able to toggle the plugin:
//
// Add the plugin to the plugins object
// const plugins = {
// plugin: require('someMarkdownPlugin'),
// }
//
// And add a corresponding entry into Setting.js
// 'markdown.plugin.mark': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ==mark== syntax')},
//
// 3. If the plugin needs application data (in ruleOptions) or needs to pass data (CSS, files to load, etc.) back
// to the application (using the context object), use the application-specific way:
//
// const imagePlugin = require('./MdToHtml/rules/image');
// markdownIt.use(imagePlugin(context, ruleOptions));
//
// Using the `context` object, a plugin can send back either CSS strings (in .css) or CSS files that need
// to be loaded (in .cssFiles). In general, the desktop app will load the CSS files and the mobile app
// will load the CSS strings.
markdownIt.use(rules.image(context, ruleOptions));
markdownIt.use(rules.checkbox(context, ruleOptions));
markdownIt.use(rules.link_open(context, ruleOptions));
markdownIt.use(rules.html_image(context, ruleOptions));
if (Setting.value('markdown.plugin.katex'))
markdownIt.use(rules.katex(context, ruleOptions));
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
markdownIt.use(rules.code_inline(context, ruleOptions));
markdownIt.use(markdownItAnchor)
if (Setting.value('markdown.plugin.toc'))
markdownIt.use(markdownItToc, { listType: 'ul' })
for (let key in plugins) {
if (Setting.value('markdown.plugin.' + key))
markdownIt.use(plugins[key]);
}
setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body);
const cssStrings = noteStyle(style, options);
for (let k in context.css) {
if (!context.css.hasOwnProperty(k)) continue;
cssStrings.push(context.css[k]);
}
for (let k in context.assetLoaders) {
if (!context.assetLoaders.hasOwnProperty(k)) continue;
context.assetLoaders[k]().catch(error => {
console.warn('MdToHtml: Error loading assets for ' + k + ': ', error.message);
});
}
if (options.userCss) cssStrings.push(options.userCss);
const styleHtml = '<style>' + cssStrings.join('\n') + '</style>';
const html = styleHtml + '<div id="rendered-md">' + renderedBody + '</div>';
const output = {
html: html,
cssFiles: Object.keys(context.cssFiles).map(k => context.cssFiles[k]),
};
// Fow now, we keep only the last entry in the cache
this.cachedOutputs_ = {};
this.cachedOutputs_[cacheKey] = output;
return output;
}
injectedJavaScript() {
return '';
}
}
module.exports = MdToHtml;

View File

@@ -0,0 +1,180 @@
module.exports = function(style, options) {
// https://necolas.github.io/normalize.css/
const normalizeCss = `
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
`;
const fontFamily = "'Avenir', 'Arial', sans-serif";
const listMarginLeft = '1.7em';
const css = `
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: ` + style.htmlLineHeight + `;
background-color: ` + style.htmlBackgroundColor + `;
font-family: ` + fontFamily + `;
padding-bottom: ` + options.paddingBottom + `;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-corner {
background: none;
}
::-webkit-scrollbar-track {
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
}
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
#rendered-md h1:first-child,
#rendered-md h2:first-child,
#rendered-md h3:first-child,
#rendered-md h4:first-child,
#rendered-md ul:first-child,
#rendered-md ol:first-child,
#rendered-md table:first-child,
#rendered-md blockquote:first-child,
#rendered-md img:first-child,
#rendered-md p:first-child {
margin-top: 0;
padding-top: 0;
}
p, h1, h2, h3, h4, h5, h6, ul, table {
margin-top: .6em;
margin-bottom: .65em;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5em;
}
h1 {
font-size: 1.5em;
font-weight: bold;
border-bottom: 1px solid ` + style.htmlDividerColor + `;
padding-bottom: .3em;
}
h2 {
font-size: 1.3em;
font-weight: bold;
padding-bottom: .1em; */
}
h3 {
font-size: 1.1em;
}
h4, h5, h6 {
font-size: 1em;
font-weight: bold;
}
a {
color: ` + style.htmlLinkColor + `;
}
ul, ol {
padding-left: 0;
margin-left: 1.7em;
}
li {
margin-bottom: .4em;
}
li p {
margin-top: 0.2em;
margin-bottom: 0;
}
.resource-icon {
display: inline-block;
position: relative;
top: .5em;
text-decoration: none;
width: 1.15em;
height: 1.45em;
margin-right: 0.4em;
background-color: ` + style.htmlColor + `;
/* Awesome Font file */
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1536 1892' xmlns='http://www.w3.org/2000/svg'><path d='M288 128C129 128 0 257 0 416v960c0 159 129 288 288 288h960c159 0 288-129 288-288V416c0-159-129-288-288-288H288zm449.168 236.572l263.434.565 263.431.562.584 73.412.584 73.412-42.732 1.504c-23.708.835-47.002 2.774-52.322 4.36-14.497 4.318-23.722 12.902-29.563 27.51l-5.12 12.802-1.403 291.717c-1.425 295.661-1.626 302.586-9.936 343.043-15.2 74-69.604 150.014-142.197 198.685-58.287 39.08-121.487 60.47-208.155 70.45-22.999 2.648-122.228 2.636-141.976-.024l-.002.006c-69.785-9.377-108.469-20.202-154.848-43.332-85.682-42.73-151.778-116.991-177.537-199.469-10.247-32.81-11.407-40.853-11.375-78.754.026-31.257.76-39.15 5.024-54.043 8.94-31.228 20.912-51.733 43.56-74.62 27.312-27.6 55.812-40.022 95.524-41.633 37.997-1.542 63.274 5.024 87.23 22.66 15.263 11.235 30.828 33.238 39.537 55.884 5.52 14.355 5.949 18.31 7.549 69.569 1.675 53.648 3.05 63.99 11.674 87.785 11.777 32.499 31.771 55.017 61.46 69.22 26.835 12.838 47.272 16.785 80.56 15.56 21.646-.798 30.212-2.135 43.208-6.741 38.682-13.708 70.96-44.553 86.471-82.635 16.027-39.348 15.995-38.647 15.947-361.595-.042-283.26-.09-286.272-4.568-296.153-10.958-24.171-22.488-28.492-81.074-30.377l-42.969-1.38v-147.95z'/></svg>");
}
blockquote {
border-left: 4px solid ` + style.htmlCodeBorderColor + `;
padding-left: 1.2em;
margin-left: 0;
opacity: .7;
}
table {
text-align: left-align;
border-collapse: collapse;
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlBackgroundColor + `;
}
td, th {
padding: .5em 1em .5em 1em;
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
font-family: ` + fontFamily + `;
}
td {
border: 1px solid ` + style.htmlCodeBorderColor + `;
}
th {
border: 1px solid ` + style.htmlCodeBorderColor + `;
border-bottom: 2px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:nth-child(even) {
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:hover {
background-color: ` + style.raisedBackgroundColor + `;
}
hr {
border: none;
border-bottom: 2px solid ` + style.htmlDividerColor + `;
}
img {
max-width: 100%;
height: auto;
}
.inline-code {
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlCodeBackgroundColor + `;
padding-right: .2em;
padding-left: .2em;
border-radius: .25em;
color: ` + style.htmlCodeColor + `;
font-size: ` + style.htmlCodeFontSize + `;
}
.highlighted-keyword {
background-color: #F3B717;
color: black;
}
@media print {
body {
height: auto !important;
}
pre {
white-space: pre-wrap;
}
.code, .inline-code {
border: 1px solid #CBCBCB;
}
}
`;
return [normalizeCss, css];
}

View File

@@ -0,0 +1,125 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
let checkboxIndex_ = -1;
const checkboxStyle = `
/* Remove the indentation from the checkboxes at the root of the document
(otherwise they are too far right), but keep it for their children to allow
nested lists. Make sure this value matches the UL margin. */
#rendered-md > ul > li.md-checkbox {
margin-left: -1.7em;
}
li.md-checkbox {
list-style-type: none;
}
li.md-checkbox input[type=checkbox] {
margin-right: 1em;
}
`;
function createPrefixTokens(Token, id, checked, label, postMessageSyntax, sourceToken) {
let token = null;
const tokens = [];
// A bit hard to handle errors here and it's unlikely that the token won't have a valid
// map parameter, but if it does set it to a very high value, which will be more easy to notice
// in calling code.
const lineIndex = sourceToken.map && sourceToken.map.length ? sourceToken.map[0] : 99999999;
const checkedString = checked ? 'checked' : 'unchecked';
const js = postMessageSyntax + "('checkboxclick:" + checkedString + ':' + lineIndex + "'); return true;";
token = new Token('checkbox_input', 'input', 0);
token.attrs = [
['type', 'checkbox'],
['id', id],
['onclick', js],
];
if (checked) token.attrs.push(['checked', 'true']);
tokens.push(token);
token = new Token('label_open', 'label', 1);
token.attrs = [['for', id]];
tokens.push(token);
if (label) {
token = new Token('text', '', 0);
token.content = label;
tokens.push(token);
}
return tokens;
}
function createSuffixTokens(Token) {
return [new Token('label_close', 'label', -1)];
}
function installRule(markdownIt, mdOptions, ruleOptions, context) {
markdownIt.core.ruler.push('checkbox', state => {
const tokens = state.tokens;
const Token = state.Token;
const checkboxPattern = /^\[([x|X| ])\] (.*)$/
let currentListItem = null;
let processedFirstInline = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'list_item_open') {
currentListItem = token;
processedFirstInline = false;
continue;
}
if (token.type === 'list_item_close') {
currentListItem = null;
processedFirstInline = false;
continue;
}
if (currentListItem && !processedFirstInline && token.type === 'inline') {
processedFirstInline = true;
const firstChild = token.children && token.children.length ? token.children[0] : null;
if (!firstChild) continue;
const matches = checkboxPattern.exec(firstChild.content);
if (!matches || matches.length < 2) continue;
checkboxIndex_++;
const checked = matches[1] !== ' ';
const id = 'md-checkbox-' + checkboxIndex_;
const label = matches.length >= 3 ? matches[2] : '';
// Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token);
const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);
token.children = token.children.concat(suffix);
// Add a class to the <li> container so that it can be targetted with CSS.
let itemClass = currentListItem.attrGet('class');
if (!itemClass) itemClass = '';
itemClass += ' md-checkbox';
currentListItem.attrSet('class', itemClass.trim());
context.css['checkbox'] = checkboxStyle;
}
}
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions, context);
};
};

View File

@@ -0,0 +1,20 @@
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.code_inline || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.code_inline = (tokens, idx, options, env, self) => {
const token = tokens[idx];
let tokenClass = token.attrGet('class');
if (!tokenClass) tokenClass = '';
tokenClass += ' inline-code';
token.attrSet('class', tokenClass.trim());
return defaultRender(tokens, idx, options, env, self);
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@@ -0,0 +1,71 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
const StringUtils = require('lib/string-utils.js');
const md5 = require('md5');
function createHighlightedTokens(Token, splitted) {
let token;
const output = [];
for (let i = 0; i < splitted.length; i++) {
const text = splitted[i];
if (!text) continue;
if (i % 2 === 0) {
token = new Token('text', '', 0);
token.content = text;
output.push(token);
} else {
token = new Token('highlighted_keyword_open', 'span', 1);
token.attrs = [['class', 'highlighted-keyword']];
output.push(token);
token = new Token('text', '', 0);
token.content = text;
output.push(token);
token = new Token('highlighted_keyword_close', 'span', -1);
output.push(token);
}
}
return output;
}
function installRule(markdownIt, mdOptions, ruleOptions) {
const divider = md5(Date.now().toString() + Math.random().toString());
markdownIt.core.ruler.push('highlight_keywords', state => {
const keywords = ruleOptions.highlightedKeywords;
if (!keywords || !keywords.length) return;
const tokens = state.tokens;
const Token = state.Token;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type !== 'inline') continue;
for (let j = 0; j < token.children.length; j++) {
const child = token.children[j];
if (child.type !== 'text') continue;
const splitted = StringUtils.surroundKeywords(keywords, child.content, divider, divider).split(divider);
const splittedTokens = createHighlightedTokens(Token, splitted);
if (splittedTokens.length <= 1) continue;
token.children = markdownIt.utils.arrayReplaceAt(token.children, j, splittedTokens);
j += splittedTokens.length - 1;
}
}
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@@ -0,0 +1,56 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function renderImageHtml(before, src, after, ruleOptions) {
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let newSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) newSrc = ruleOptions.resourceBaseUrl + newSrc;
return '<img ' + before + ' data-resource-id="' + resource.id + '" src="' + newSrc + '" ' + after + '/>';
}
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
}
function installRule(markdownIt, mdOptions, ruleOptions) {
const htmlBlockDefaultRender = markdownIt.renderer.rules.html_block || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
const htmlInlineDefaultRender = markdownIt.renderer.rules.html_inline || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
const imageRegex = /<img(.*?)src=["'](.*?)["'](.*?)\/>/
const handleImageTags = function(defaultRender) {
return function(tokens, idx, options, env, self) {
const token = tokens[idx];
const content = token.content;
if (!content.match(imageRegex)) return defaultRender(tokens, idx, options, env, self);
return content.replace(imageRegex, (v, before, src, after) => {
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
return renderImageHtml(before, src, after, ruleOptions);
});
}
}
// It seems images sometimes are inline, sometimes a block
// to make sure they both render correctly.
markdownIt.renderer.rules.html_block = handleImageTags(htmlBlockDefaultRender);
markdownIt.renderer.rules.html_inline = handleImageTags(htmlInlineDefaultRender);
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@@ -0,0 +1,36 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const src = utils.getAttr(token.attrs, 'src');
const title = utils.getAttr(token.attrs, 'title');
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let realSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) realSrc = ruleOptions.resourceBaseUrl + realSrc;
let output = '<img data-from-md data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + realSrc + '"/>';
return output;
}
return defaultRender(tokens, idx, options, env, self);
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

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