You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-17 00:33:59 +02:00
Compare commits
39 Commits
server_fil
...
hide_show_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365ea86787 | ||
|
|
638e55aa6e | ||
|
|
0be8cdf760 | ||
|
|
545940f545 | ||
|
|
d58f39823a | ||
|
|
f9fb1b8a81 | ||
|
|
56ded0062a | ||
|
|
83b29d7c51 | ||
|
|
1ec0746263 | ||
|
|
568d11bddf | ||
|
|
97b25ac99d | ||
|
|
7f1d3d8a5d | ||
|
|
fde201fbe9 | ||
|
|
694a1b4ede | ||
|
|
20d126b39d | ||
|
|
8ca9c3092a | ||
|
|
d2771029a3 | ||
|
|
4128c53fcf | ||
|
|
a14410b28c | ||
|
|
d1f8520e6e | ||
|
|
d76746b8e4 | ||
|
|
89d173b460 | ||
|
|
81aba8b8b0 | ||
|
|
f48697572d | ||
|
|
e61e8b7b94 | ||
|
|
1deab7e8d1 | ||
|
|
86b28b5ecf | ||
|
|
938e723434 | ||
|
|
ee2ec28cd4 | ||
|
|
9dc505e85b | ||
|
|
3df31584af | ||
|
|
a7e3b381cb | ||
|
|
70381a233b | ||
|
|
2966fe0df2 | ||
|
|
143f0b4bc5 | ||
|
|
24ec3b8897 | ||
|
|
01aa4f9d5e | ||
|
|
520efdcb39 | ||
|
|
34a99f738c |
@@ -803,6 +803,9 @@ packages/app-mobile/components/screens/Note.js.map
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.d.ts
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/root.d.ts
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/root.js.map
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.d.ts
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js.map
|
||||
@@ -932,6 +935,9 @@ packages/lib/fs-driver-base.js.map
|
||||
packages/lib/fs-driver-node.d.ts
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fs-driver-node.js.map
|
||||
packages/lib/import-enex-md-gen.d.ts
|
||||
packages/lib/import-enex-md-gen.js
|
||||
packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1454,6 +1460,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@@ -1463,9 +1472,21 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/ownerHandler.d.ts
|
||||
packages/server/src/middleware/ownerHandler.js
|
||||
packages/server/src/middleware/ownerHandler.js.map
|
||||
packages/server/src/middleware/routeHandler.d.ts
|
||||
packages/server/src/middleware/routeHandler.js
|
||||
packages/server/src/middleware/routeHandler.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@@ -1481,6 +1502,15 @@ packages/server/src/models/ChangeModel.test.js.map
|
||||
packages/server/src/models/FileModel.d.ts
|
||||
packages/server/src/models/FileModel.js
|
||||
packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@@ -1526,6 +1556,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
@@ -1598,6 +1631,9 @@ packages/server/src/utils/time.js.map
|
||||
packages/server/src/utils/types.d.ts
|
||||
packages/server/src/utils/types.js
|
||||
packages/server/src/utils/types.js.map
|
||||
packages/server/src/utils/urlUtils.d.ts
|
||||
packages/server/src/utils/urlUtils.js
|
||||
packages/server/src/utils/urlUtils.js.map
|
||||
packages/server/src/utils/uuidgen.d.ts
|
||||
packages/server/src/utils/uuidgen.js
|
||||
packages/server/src/utils/uuidgen.js.map
|
||||
|
||||
@@ -65,7 +65,12 @@ module.exports = {
|
||||
'no-var': ['error'],
|
||||
'no-new-func': ['error'],
|
||||
'import/prefer-default-export': ['error'],
|
||||
'import/first': ['error'],
|
||||
|
||||
// This rule should not be enabled since it matters in what order
|
||||
// imports are done, in particular in relation to the shim.setReact
|
||||
// call, which should be done first, but this rule might move it down.
|
||||
// 'import/first': ['error'],
|
||||
|
||||
'no-array-constructor': ['error'],
|
||||
'radix': ['error'],
|
||||
|
||||
|
||||
36
.gitignore
vendored
36
.gitignore
vendored
@@ -792,6 +792,9 @@ packages/app-mobile/components/screens/Note.js.map
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.d.ts
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/root.d.ts
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/root.js.map
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.d.ts
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js.map
|
||||
@@ -921,6 +924,9 @@ packages/lib/fs-driver-base.js.map
|
||||
packages/lib/fs-driver-node.d.ts
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fs-driver-node.js.map
|
||||
packages/lib/import-enex-md-gen.d.ts
|
||||
packages/lib/import-enex-md-gen.js
|
||||
packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1443,6 +1449,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@@ -1452,9 +1461,21 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/ownerHandler.d.ts
|
||||
packages/server/src/middleware/ownerHandler.js
|
||||
packages/server/src/middleware/ownerHandler.js.map
|
||||
packages/server/src/middleware/routeHandler.d.ts
|
||||
packages/server/src/middleware/routeHandler.js
|
||||
packages/server/src/middleware/routeHandler.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@@ -1470,6 +1491,15 @@ packages/server/src/models/ChangeModel.test.js.map
|
||||
packages/server/src/models/FileModel.d.ts
|
||||
packages/server/src/models/FileModel.js
|
||||
packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@@ -1515,6 +1545,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
@@ -1587,6 +1620,9 @@ packages/server/src/utils/time.js.map
|
||||
packages/server/src/utils/types.d.ts
|
||||
packages/server/src/utils/types.js
|
||||
packages/server/src/utils/types.js.map
|
||||
packages/server/src/utils/urlUtils.d.ts
|
||||
packages/server/src/utils/urlUtils.js
|
||||
packages/server/src/utils/urlUtils.js.map
|
||||
packages/server/src/utils/uuidgen.d.ts
|
||||
packages/server/src/utils/uuidgen.js
|
||||
packages/server/src/utils/uuidgen.js.map
|
||||
|
||||
74
README.md
74
README.md
@@ -109,6 +109,8 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
|
||||
- [Plugin Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/plugins.md)
|
||||
- [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md)
|
||||
- [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md)
|
||||
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
|
||||
|
||||
- Google Summer of Code 2020
|
||||
|
||||
@@ -436,45 +438,45 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
 | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [أحمد باشا إبراهيم](mailto:fi_ahmed_bacha@esi.dz) | 77%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 33%
|
||||
 | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 79%
|
||||
 | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 64%
|
||||
 | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 92%
|
||||
 | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [أحمد باشا إبراهيم](mailto:fi_ahmed_bacha@esi.dz) | 75%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 32%
|
||||
 | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 77%
|
||||
 | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 62%
|
||||
 | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 89%
|
||||
 | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Hrvoje Mandić](mailto:trbuhom@net.hr) | 26%
|
||||
 | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 96%
|
||||
 | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 79%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Ettore Atalan](mailto:atalanttore@users.noreply.github.com) | 96%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 63%
|
||||
 | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 93%
|
||||
 | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 77%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Ettore Atalan](mailto:atalanttore@users.noreply.github.com) | 95%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 61%
|
||||
 | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
 | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
 | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 96%
|
||||
 | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 36%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | | 97%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 96%
|
||||
 | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 42%
|
||||
 | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Fathy AR](mailto:16875937+fathyar@users.noreply.github.com) | 88%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 97%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 33%
|
||||
 | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 96%
|
||||
 | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 85%
|
||||
 | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 79%
|
||||
 | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | | 95%
|
||||
 | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Renato Nunes Bastos](mailto:rnbastos@gmail.com) | 93%
|
||||
 | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [João Duarte](mailto:jduar@protonmail.com) | 95%
|
||||
 | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 74%
|
||||
 | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | | 41%
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 68%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 50%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 82%
|
||||
 | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 94%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 92%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 69%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 97%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 91%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 97%
|
||||
 | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 99%
|
||||
 | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 35%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | | 94%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
|
||||
 | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 40%
|
||||
 | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Fathy AR](mailto:16875937+fathyar@users.noreply.github.com) | 85%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 96%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 32%
|
||||
 | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 93%
|
||||
 | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 82%
|
||||
 | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 77%
|
||||
 | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | | 92%
|
||||
 | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Renato Nunes Bastos](mailto:rnbastos@gmail.com) | 91%
|
||||
 | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [João Duarte](mailto:jduar@protonmail.com) | 92%
|
||||
 | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 72%
|
||||
 | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | | 40%
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 66%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 49%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 79%
|
||||
 | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 89%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 96%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 67%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 99%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 95%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 96%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
1
packages/app-cli/tests/enex_to_md/svg.html
Normal file
1
packages/app-cli/tests/enex_to_md/svg.html
Normal file
@@ -0,0 +1 @@
|
||||
<img src="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='16px' height='16px' viewBox='0 0 24 24' data-evernote-id='0' class='js-evernote-checked'%3e%3cg transform='translate(0%2c 0)' data-evernote-id='18' class='js-evernote-checked'%3e%3cpolygon fill='none' stroke='%23343434' stroke-width='2' stroke-linecap='square' stroke-miterlimit='10' points='12%2c2.6 15%2c9 21.4%2c9 16.7%2c13.9 18.6%2c21.4 12%2c17.6 5.4%2c21.4 7.3%2c13.9 2.6%2c9 9%2c9 ' stroke-linejoin='miter' data-evernote-id='19' class='js-evernote-checked'%3e%3c/polygon%3e%3c/g%3e%3c/svg%3e"/>
|
||||
1
packages/app-cli/tests/enex_to_md/svg.md
Normal file
1
packages/app-cli/tests/enex_to_md/svg.md
Normal file
@@ -0,0 +1 @@
|
||||

|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import joplin from 'api';
|
||||
import { ToolbarButtonLocation } from 'api/types';
|
||||
|
||||
const uslug = require('uslug');
|
||||
|
||||
@@ -91,6 +92,18 @@ joplin.plugins.register({
|
||||
updateTocView();
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'toggleToc',
|
||||
label: 'Toggle TOC',
|
||||
iconName: 'fas fa-drum',
|
||||
execute: async () => {
|
||||
const isVisible = await (panels as any).visible(view);
|
||||
(panels as any).show(view, !isVisible);
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.views.toolbarButtons.create('toggleToc', 'toggleToc', ToolbarButtonLocation.NoteToolbar);
|
||||
|
||||
updateTocView();
|
||||
},
|
||||
});
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
@@ -22,6 +22,9 @@ import { LayoutItem } from './gui/ResizableLayout/utils/types';
|
||||
import stateToWhenClauseContext from './services/commands/stateToWhenClauseContext';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import produce from 'immer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
|
||||
|
||||
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
|
||||
const MasterKey = require('@joplin/lib/models/MasterKey');
|
||||
@@ -247,6 +250,29 @@ class Application extends BaseApplication {
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET_ITEM_PROP':
|
||||
|
||||
{
|
||||
let newLayout = produce(state.mainLayout, (draftLayout: LayoutItem) => {
|
||||
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
|
||||
if (item.key === action.itemKey) {
|
||||
(item as any)[action.propName] = action.propValue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (newLayout !== state.mainLayout) newLayout = validateLayout(newLayout);
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: newLayout,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_ADD':
|
||||
|
||||
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
||||
|
||||
@@ -214,7 +214,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
let output = null;
|
||||
|
||||
try {
|
||||
output = loadLayout(userLayout, defaultLayout, rootLayoutSize);
|
||||
output = loadLayout(Object.keys(userLayout).length ? userLayout : null, defaultLayout, rootLayoutSize);
|
||||
|
||||
if (!findItemByKey(output, 'sideBar') || !findItemByKey(output, 'noteList') || !findItemByKey(output, 'editor')) {
|
||||
throw new Error('"sideBar", "noteList" and "editor" must be present in the layout');
|
||||
|
||||
@@ -19,7 +19,7 @@ const taboverride = require('taboverride');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
// const { clipboard } = require('electron');
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
@@ -1015,19 +1015,22 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(_event: any) {
|
||||
// It seems "paste as text" is now handled automatically by
|
||||
// either Chrome, Electron and/or TinyMCE so the code below
|
||||
// should not longer be necessary. Also fixes
|
||||
function onKeyDown(event: any) {
|
||||
// It seems "paste as text" is handled automatically by
|
||||
// on Windows so the code below so we need to run the below
|
||||
// code only on macOS (and maybe Linux). If we were to run
|
||||
// this on Windows we would have this double-paste issue:
|
||||
// https://github.com/laurent22/joplin/issues/4243
|
||||
|
||||
// Handle "paste as text". Note that when pressing CtrlOrCmd+Shift+V it's going
|
||||
// to trigger the "keydown" event but not the "paste" event, so it's ok to process
|
||||
// it here and we don't need to do anything special in onPaste
|
||||
// if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
// const pastedText = clipboard.readText();
|
||||
// if (pastedText) editor.insertContent(pastedText);
|
||||
// }
|
||||
if (!shim.isWindows()) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
const pastedText = clipboard.readText();
|
||||
if (pastedText) editor.insertContent(pastedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.on('keyup', onKeyUp);
|
||||
|
||||
@@ -16,8 +16,4 @@ export default function findItemByKey(layout: LayoutItem, key: string): LayoutIt
|
||||
}
|
||||
|
||||
return recurseFind(layout);
|
||||
|
||||
// const output = recurseFind(layout);
|
||||
// if (!output) throw new Error(`Could not find item "${key}"`);
|
||||
// return output;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { LayoutItem } from './types';
|
||||
|
||||
type ItemItemCallback = (itemIndex: number, item: LayoutItem, parent: LayoutItem)=> boolean;
|
||||
|
||||
// Callback should return `true` if iteration should continue, or `false` if it
|
||||
// should stop
|
||||
export default function iterateItems(layout: LayoutItem, callback: ItemItemCallback) {
|
||||
const result = callback(0, layout, null);
|
||||
if (result === false) return;
|
||||
|
||||
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# It could be used to develop plugins too.
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/register_command"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/toc"
|
||||
npm i --prefix="$PLUGIN_PATH" && npm start -- --dev-plugins "$PLUGIN_PATH"
|
||||
@@ -138,8 +138,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097614
|
||||
versionName "1.6.0"
|
||||
versionCode 2097618
|
||||
versionName "1.6.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// So there's basically still a one way flux: React => SQLite => Redux => React
|
||||
|
||||
import { LogBox, AppRegistry } from 'react-native';
|
||||
const { Root } = require('./root.js');
|
||||
const Root = require('./root').default;
|
||||
|
||||
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
|
||||
// or don't really matter. Because we want important warnings to actually be fixed, we disable
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import setUpQuickActions from './setUpQuickActions';
|
||||
import PluginAssetsLoader from './PluginAssetsLoader';
|
||||
|
||||
const React = require('react');
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
import shim from '@joplin/lib/shim';
|
||||
shim.setReact(React);
|
||||
|
||||
import setUpQuickActions from './setUpQuickActions';
|
||||
import PluginAssetsLoader from './PluginAssetsLoader';
|
||||
import AlarmService from '@joplin/lib/services/AlarmService';
|
||||
import Alarm from '@joplin/lib/models/Alarm';
|
||||
import time from '@joplin/lib/time';
|
||||
import Logger, { TargetType } from '@joplin/lib/Logger';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseService from '@joplin/lib/services/BaseService';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import NoteScreen from './components/screens/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
import ShareExtension from './utils/ShareExtension';
|
||||
import handleShared from './utils/shareHandler';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
|
||||
import KeychainServiceDriverMobile from '@joplin/lib/services/keychain/KeychainServiceDriver.mobile';
|
||||
import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/locale';
|
||||
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
|
||||
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
|
||||
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
|
||||
const SafeAreaView = require('./components/SafeAreaView');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
const { BackButtonService } = require('./services/back-button.js');
|
||||
const NavService = require('@joplin/lib/services/NavService.js');
|
||||
const AlarmService = require('@joplin/lib/services/AlarmService.js').default;
|
||||
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
|
||||
const Alarm = require('@joplin/lib/models/Alarm').default;
|
||||
const { createStore, applyMiddleware } = require('redux');
|
||||
const reduxSharedMiddleware = require('@joplin/lib/components/shared/reduxSharedMiddleware');
|
||||
const { shimInit } = require('./utils/shim-init-react.js');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const { AppNav } = require('./components/app-nav.js');
|
||||
const Logger = require('@joplin/lib/Logger').default;
|
||||
const Note = require('@joplin/lib/models/Note.js');
|
||||
const Folder = require('@joplin/lib/models/Folder.js');
|
||||
const BaseSyncTarget = require('@joplin/lib/BaseSyncTarget.js');
|
||||
@@ -29,16 +48,11 @@ const NoteTag = require('@joplin/lib/models/NoteTag.js');
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem.js');
|
||||
const MasterKey = require('@joplin/lib/models/MasterKey.js');
|
||||
const Revision = require('@joplin/lib/models/Revision.js');
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const BaseService = require('@joplin/lib/services/BaseService').default;
|
||||
const ResourceService = require('@joplin/lib/services/ResourceService').default;
|
||||
const RevisionService = require('@joplin/lib/services/RevisionService');
|
||||
const KvStore = require('@joplin/lib/services/KvStore').default;
|
||||
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
|
||||
const { Database } = require('@joplin/lib/database.js');
|
||||
const { NotesScreen } = require('./components/screens/notes.js');
|
||||
const { TagsScreen } = require('./components/screens/tags.js');
|
||||
const NoteScreen = require('./components/screens/Note').default;
|
||||
const { ConfigScreen } = require('./components/screens/config.js');
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
const { LogScreen } = require('./components/screens/log.js');
|
||||
@@ -47,31 +61,18 @@ const { SearchScreen } = require('./components/screens/search.js');
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
const { EncryptionConfigScreen } = require('./components/screens/encryption-config.js');
|
||||
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
||||
const UpgradeSyncTargetScreen = require('./components/screens/UpgradeSyncTargetScreen').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const { MenuContext } = require('react-native-popup-menu');
|
||||
const { SideMenu } = require('./components/side-menu.js');
|
||||
const { SideMenuContent } = require('./components/side-menu-content.js');
|
||||
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
||||
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { setLocale, closestSupportedLocale, defaultLocale } = require('@joplin/lib/locale');
|
||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||
const PoorManIntervals = require('@joplin/lib/PoorManIntervals').default;
|
||||
const reducer = require('@joplin/lib/reducer').default;
|
||||
const { defaultState } = require('@joplin/lib/reducer');
|
||||
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
const ShareExtension = require('./utils/ShareExtension.js').default;
|
||||
const handleShared = require('./utils/shareHandler').default;
|
||||
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher');
|
||||
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
|
||||
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
|
||||
const { themeStyle } = require('./components/global-style.js');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
|
||||
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
||||
const KeychainServiceDriverMobile = require('@joplin/lib/services/keychain/KeychainServiceDriver.mobile').default;
|
||||
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry.js');
|
||||
const SyncTargetOneDrive = require('@joplin/lib/SyncTargetOneDrive.js');
|
||||
@@ -87,15 +88,16 @@ SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
|
||||
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
|
||||
|
||||
const FsDriverRN = require('./utils/fs-driver-rn.js').FsDriverRN;
|
||||
const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker');
|
||||
const EncryptionService = require('@joplin/lib/services/EncryptionService');
|
||||
const MigrationService = require('@joplin/lib/services/MigrationService');
|
||||
|
||||
let storeDispatch = function() {};
|
||||
let storeDispatch = function(_action: any) {};
|
||||
|
||||
const logReducerAction = function(action) {
|
||||
const logReducerAction = function(action: any) {
|
||||
if (['SIDE_MENU_OPEN_PERCENT', 'SYNC_REPORT_UPDATE'].indexOf(action.type) >= 0) return;
|
||||
|
||||
const msg = [action.type];
|
||||
@@ -104,7 +106,7 @@ const logReducerAction = function(action) {
|
||||
// reg.logger().debug('Reducer action', msg.join(', '));
|
||||
};
|
||||
|
||||
const generalMiddleware = store => next => async (action) => {
|
||||
const generalMiddleware = (store: any) => (next: any) => async (action: any) => {
|
||||
logReducerAction(action);
|
||||
PoorManIntervals.update(); // This function needs to be called regularly so put it here
|
||||
|
||||
@@ -167,9 +169,9 @@ const generalMiddleware = store => next => async (action) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const navHistory = [];
|
||||
const navHistory: any[] = [];
|
||||
|
||||
function historyCanGoBackTo(route) {
|
||||
function historyCanGoBackTo(route: any) {
|
||||
if (route.routeName === 'Note') return false;
|
||||
if (route.routeName === 'Folder') return false;
|
||||
|
||||
@@ -194,13 +196,14 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
noteSideMenuOptions: null,
|
||||
});
|
||||
|
||||
const appReducer = (state = appDefaultState, action) => {
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
let newState = state;
|
||||
let historyGoingBack = false;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
// @ts-ignore
|
||||
case 'NAV_BACK':
|
||||
|
||||
{
|
||||
@@ -224,7 +227,7 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
{
|
||||
const currentRoute = state.route;
|
||||
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute, action)) {
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
||||
// If the route *name* is the same (even if the other parameters are different), we
|
||||
// overwrite the last route in the history with the current one. If the route name
|
||||
// is different, we push a new history entry.
|
||||
@@ -368,7 +371,7 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
||||
storeDispatch = store.dispatch;
|
||||
|
||||
function resourceFetcher_downloadComplete(event) {
|
||||
function resourceFetcher_downloadComplete(event: any) {
|
||||
if (event.encrypted) {
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
}
|
||||
@@ -378,9 +381,10 @@ function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
|
||||
ResourceFetcher.instance().scheduleAutoAddResources();
|
||||
}
|
||||
|
||||
async function initialize(dispatch) {
|
||||
async function initialize(dispatch: Function) {
|
||||
shimInit();
|
||||
|
||||
// @ts-ignore
|
||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||
Setting.setConstant('appType', 'mobile');
|
||||
@@ -391,18 +395,18 @@ async function initialize(dispatch) {
|
||||
await logDatabase.exec(Logger.databaseCreateTableSql());
|
||||
|
||||
const mainLogger = new Logger();
|
||||
mainLogger.addTarget('database', { database: logDatabase, source: 'm' });
|
||||
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
|
||||
mainLogger.setLevel(Logger.LEVEL_INFO);
|
||||
|
||||
if (Setting.value('env') == 'dev') {
|
||||
mainLogger.addTarget('console');
|
||||
mainLogger.addTarget(TargetType.Console);
|
||||
mainLogger.setLevel(Logger.LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
Logger.initializeGlobalLogger(mainLogger);
|
||||
|
||||
reg.setLogger(mainLogger);
|
||||
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { alert(message); });
|
||||
|
||||
BaseService.logger_ = mainLogger;
|
||||
// require('@joplin/lib/ntpDate').setLogger(reg.logger());
|
||||
@@ -411,9 +415,9 @@ async function initialize(dispatch) {
|
||||
reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`);
|
||||
|
||||
const dbLogger = new Logger();
|
||||
dbLogger.addTarget('database', { database: logDatabase, source: 'm' });
|
||||
dbLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
|
||||
if (Setting.value('env') == 'dev') {
|
||||
dbLogger.addTarget('console');
|
||||
dbLogger.addTarget(TargetType.Console);
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO); // Set to LEVEL_DEBUG for full SQL queries
|
||||
} else {
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO);
|
||||
@@ -452,7 +456,7 @@ async function initialize(dispatch) {
|
||||
if (Setting.value('env') == 'prod') {
|
||||
await db.open({ name: 'joplin.sqlite' });
|
||||
} else {
|
||||
await db.open({ name: 'joplin-76.sqlite' });
|
||||
await db.open({ name: 'joplin-100.sqlite' });
|
||||
|
||||
// await db.clearForTesting();
|
||||
}
|
||||
@@ -562,7 +566,7 @@ async function initialize(dispatch) {
|
||||
reg.setupRecurrentSync();
|
||||
|
||||
PoorManIntervals.setTimeout(() => {
|
||||
AlarmService.garbageCollect();
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
ResourceService.runInBackground();
|
||||
@@ -584,7 +588,7 @@ async function initialize(dispatch) {
|
||||
reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
AlarmService.updateAllNotifications();
|
||||
void AlarmService.updateAllNotifications();
|
||||
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
@@ -654,7 +658,7 @@ class AppComponent extends React.Component {
|
||||
|
||||
BackButtonService.initialize(this.backButtonHandler_);
|
||||
|
||||
AlarmService.setInAppNotificationHandler(async (alarmId) => {
|
||||
AlarmService.setInAppNotificationHandler(async (alarmId: string) => {
|
||||
const alarm = await Alarm.load(alarmId);
|
||||
const notification = await Alarm.makeNotification(alarm);
|
||||
this.dropdownAlert_.alertWithType('info', notification.title, notification.body ? notification.body : '');
|
||||
@@ -666,7 +670,7 @@ class AppComponent extends React.Component {
|
||||
if (sharedData) {
|
||||
reg.logger().info('Received shared data');
|
||||
if (this.props.selectedFolderId) {
|
||||
handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
|
||||
await handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
|
||||
} else {
|
||||
reg.logger.info('Cannot handle share - default folder id is not set');
|
||||
}
|
||||
@@ -677,7 +681,7 @@ class AppComponent extends React.Component {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: any) {
|
||||
if (this.props.showSideMenu !== prevProps.showSideMenu) {
|
||||
Animated.timing(this.state.sideMenuContentOpacity, {
|
||||
toValue: this.props.showSideMenu ? 0.5 : 0,
|
||||
@@ -707,14 +711,14 @@ class AppComponent extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
if (newProps.syncStarted != this.lastSyncStarted_) {
|
||||
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
|
||||
this.lastSyncStarted_ = newProps.syncStarted;
|
||||
}
|
||||
}
|
||||
|
||||
sideMenu_change(isOpen) {
|
||||
sideMenu_change(isOpen: boolean) {
|
||||
// Make sure showSideMenu property of state is updated
|
||||
// when the menu is open/closed.
|
||||
this.props.dispatch({
|
||||
@@ -759,8 +763,8 @@ class AppComponent extends React.Component {
|
||||
menu={sideMenuContent}
|
||||
edgeHitWidth={5}
|
||||
menuPosition={menuPosition}
|
||||
onChange={(isOpen) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent) => {
|
||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent: number) => {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
||||
value: percent,
|
||||
@@ -773,7 +777,7 @@ class AppComponent extends React.Component {
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
<AppNav screens={appNavInit} />
|
||||
</View>
|
||||
<DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
|
||||
</SafeAreaView>
|
||||
</MenuContext>
|
||||
@@ -783,7 +787,7 @@ class AppComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = (state: any) => {
|
||||
return {
|
||||
historyCanGoBack: state.historyCanGoBack,
|
||||
showSideMenu: state.showSideMenu,
|
||||
@@ -799,7 +803,7 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const App = connect(mapStateToProps)(AppComponent);
|
||||
|
||||
class Root extends React.Component {
|
||||
export default class Root extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
@@ -808,5 +812,3 @@ class Root extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Root };
|
||||
@@ -24,9 +24,9 @@ export default async (sharedData: SharedData, folderId: string, dispatch: Functi
|
||||
// below will do nothing (because routeName wouldn't change)
|
||||
// Then we wait a bit for the state to be set correctly, and
|
||||
// finally we go to the new note.
|
||||
await dispatch({ type: 'NAV_BACK' });
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
|
||||
await dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
const newNote = await Note.save({
|
||||
parent_id: folderId,
|
||||
|
||||
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
2
packages/generator-joplin/package-lock.json
generated
2
packages/generator-joplin/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -66,7 +66,7 @@ class BaseModel {
|
||||
public static TYPE_SMART_FILTER = ModelType.SmartFilter;
|
||||
public static TYPE_COMMAND = ModelType.Command;
|
||||
|
||||
protected static dispatch: Function = function() {};
|
||||
public static dispatch: Function = function() {};
|
||||
private static saveMutexes_: any = {};
|
||||
|
||||
private static db_: any;
|
||||
|
||||
@@ -180,7 +180,7 @@ class Logger {
|
||||
if (level == LogLevel.Warn) fn = 'warn';
|
||||
if (level == LogLevel.Info) fn = 'info';
|
||||
const consoleObj = target.console ? target.console : console;
|
||||
let items:any[] = [];
|
||||
let items: any[] = [];
|
||||
|
||||
if (target.format) {
|
||||
const format = level === LogLevel.Info && target.formatInfo ? target.formatInfo : target.format;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import markdownUtils from './markdownUtils';
|
||||
import { ResourceEntity } from './services/database/types';
|
||||
const stringPadding = require('string-padding');
|
||||
const stringToStream = require('string-to-stream');
|
||||
const resourceUtils = require('./resourceUtils.js');
|
||||
@@ -8,7 +10,51 @@ const NEWLINE = '[[NEWLINE]]';
|
||||
const NEWLINE_MERGED = '[[MERGED]]';
|
||||
const SPACE = '[[SPACE]]';
|
||||
|
||||
function processMdArrayNewLines(md) {
|
||||
enum SectionType {
|
||||
Text = 'text',
|
||||
Tr = 'tr',
|
||||
Td = 'td',
|
||||
Table = 'table',
|
||||
Caption = 'caption',
|
||||
Hidden = 'hidden',
|
||||
Code = 'code',
|
||||
}
|
||||
|
||||
interface Section {
|
||||
type: SectionType;
|
||||
parent: Section;
|
||||
lines: any[];
|
||||
isHeader?: boolean;
|
||||
}
|
||||
|
||||
interface ParserStateTag {
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface ParserStateList {
|
||||
tag: string;
|
||||
counter: number;
|
||||
startedText: boolean;
|
||||
}
|
||||
|
||||
interface ParserState {
|
||||
inCode: boolean[];
|
||||
inPre: boolean;
|
||||
inQuote: boolean;
|
||||
lists: ParserStateList[];
|
||||
anchorAttributes: any[];
|
||||
spanAttributes: string[];
|
||||
tags: ParserStateTag[];
|
||||
currentCode?: string;
|
||||
}
|
||||
|
||||
interface EnexXmlToMdArrayResult {
|
||||
content: Section;
|
||||
resources: ResourceEntity[];
|
||||
}
|
||||
|
||||
function processMdArrayNewLines(md: string[]): string {
|
||||
while (md.length && md[0] == BLOCK_OPEN) {
|
||||
md.shift();
|
||||
}
|
||||
@@ -102,7 +148,7 @@ function processMdArrayNewLines(md) {
|
||||
if (!output.trim().length) return '';
|
||||
|
||||
// To simplify the result, we only allow up to one empty line between blocks of text
|
||||
const mergeMultipleNewLines = function(lines) {
|
||||
const mergeMultipleNewLines = function(lines: string[]) {
|
||||
const output = [];
|
||||
let newlineCount = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -159,23 +205,23 @@ function processMdArrayNewLines(md) {
|
||||
// differently than if there's a newlines between them. So the function below parses the almost final MD and add new lines depending
|
||||
// on various rules.
|
||||
|
||||
const isHeading = function(line) {
|
||||
const isHeading = function(line: string) {
|
||||
return !!line.match(/^#+\s/);
|
||||
};
|
||||
|
||||
const isListItem = function(line) {
|
||||
const isListItem = function(line: string) {
|
||||
return line && line.trim().indexOf('- ') === 0;
|
||||
};
|
||||
|
||||
const isCodeLine = function(line) {
|
||||
const isCodeLine = function(line: string) {
|
||||
return line && line.indexOf('\t') === 0;
|
||||
};
|
||||
|
||||
const isTableLine = function(line) {
|
||||
const isTableLine = function(line: string) {
|
||||
return line.indexOf('| ') === 0;
|
||||
};
|
||||
|
||||
const isPlainParagraph = function(line) {
|
||||
const isPlainParagraph = function(line: string) {
|
||||
// Note: if a line is no longer than 80 characters, we don't consider it's a paragraph, which
|
||||
// means no newlines will be added before or after. This is to handle text that has been
|
||||
// written with "hard" new lines.
|
||||
@@ -189,7 +235,7 @@ const isPlainParagraph = function(line) {
|
||||
return true;
|
||||
};
|
||||
|
||||
function formatMdLayout(lines) {
|
||||
function formatMdLayout(lines: string[]) {
|
||||
let previous = '';
|
||||
const newLines = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -235,13 +281,13 @@ function formatMdLayout(lines) {
|
||||
return newLines;
|
||||
}
|
||||
|
||||
function isWhiteSpace(c) {
|
||||
function isWhiteSpace(c: string): boolean {
|
||||
return c == '\n' || c == '\r' || c == '\v' || c == '\f' || c == '\t' || c == ' ';
|
||||
}
|
||||
|
||||
// Like QString::simpified(), except that it preserves non-breaking spaces (which
|
||||
// Evernote uses for identation, etc.)
|
||||
function simplifyString(s) {
|
||||
function simplifyString(s: string): string {
|
||||
let output = '';
|
||||
let previousWhite = false;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
@@ -261,7 +307,7 @@ function simplifyString(s) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function collapseWhiteSpaceAndAppend(lines, state, text) {
|
||||
function collapseWhiteSpaceAndAppend(lines: string[], state: any, text: string) {
|
||||
if (state.inCode.length) {
|
||||
lines.push(text);
|
||||
} else {
|
||||
@@ -296,7 +342,7 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function tagAttributeToMdText(attr) {
|
||||
function tagAttributeToMdText(attr: string): string {
|
||||
// HTML attributes may contain newlines so remove them.
|
||||
// https://github.com/laurent22/joplin/issues/1583
|
||||
if (!attr) return '';
|
||||
@@ -305,7 +351,7 @@ function tagAttributeToMdText(attr) {
|
||||
return attr;
|
||||
}
|
||||
|
||||
function addResourceTag(lines, resource, alt = '') {
|
||||
function addResourceTag(lines: string[], resource: ResourceEntity, alt: string = ''): string[] {
|
||||
// Note: refactor to use Resource.markdownTag
|
||||
|
||||
if (!alt) alt = resource.title;
|
||||
@@ -326,50 +372,50 @@ function addResourceTag(lines, resource, alt = '') {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function isBlockTag(n) {
|
||||
function isBlockTag(n: string) {
|
||||
return ['div', 'p', 'dl', 'dd', 'dt', 'center', 'address'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isStrongTag(n) {
|
||||
function isStrongTag(n: string) {
|
||||
return n == 'strong' || n == 'b' || n == 'big';
|
||||
}
|
||||
|
||||
function isStrikeTag(n) {
|
||||
function isStrikeTag(n: string) {
|
||||
return n == 'strike' || n == 's' || n == 'del';
|
||||
}
|
||||
|
||||
function isEmTag(n) {
|
||||
function isEmTag(n: string) {
|
||||
return n == 'em' || n == 'i' || n == 'u';
|
||||
}
|
||||
|
||||
function isAnchor(n) {
|
||||
function isAnchor(n: string) {
|
||||
return n == 'a';
|
||||
}
|
||||
|
||||
function isIgnoredEndTag(n) {
|
||||
function isIgnoredEndTag(n: string) {
|
||||
return ['en-note', 'en-todo', 'body', 'html', 'font', 'br', 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isListTag(n) {
|
||||
function isListTag(n: string) {
|
||||
return n == 'ol' || n == 'ul';
|
||||
}
|
||||
|
||||
// Elements that don't require any special treatment beside adding a newline character
|
||||
function isNewLineOnlyEndTag(n) {
|
||||
function isNewLineOnlyEndTag(n: string) {
|
||||
return ['div', 'p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'dd', 'dt', 'center', 'address'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isInlineCodeTag(n) {
|
||||
function isInlineCodeTag(n: string) {
|
||||
return ['samp', 'kbd'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isNewLineBlock(s) {
|
||||
function isNewLineBlock(s: string) {
|
||||
return s == BLOCK_OPEN || s == BLOCK_CLOSE;
|
||||
}
|
||||
|
||||
function attributeToLowerCase(node) {
|
||||
function attributeToLowerCase(node: any) {
|
||||
if (!node.attributes) return {};
|
||||
const output = {};
|
||||
const output: any = {};
|
||||
for (const n in node.attributes) {
|
||||
if (!node.attributes.hasOwnProperty(n)) continue;
|
||||
output[n.toLowerCase()] = node.attributes[n];
|
||||
@@ -377,13 +423,13 @@ function attributeToLowerCase(node) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function isInvisibleBlock(attributes) {
|
||||
function isInvisibleBlock(attributes: any) {
|
||||
const style = attributes.style;
|
||||
if (!style) return false;
|
||||
return !!style.match(/display:[\s\S]*none/);
|
||||
}
|
||||
|
||||
function isSpanWithStyle(attributes) {
|
||||
function isSpanWithStyle(attributes: any) {
|
||||
if (attributes != undefined) {
|
||||
if ('style' in attributes) {
|
||||
return true;
|
||||
@@ -391,9 +437,10 @@ function isSpanWithStyle(attributes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSpanStyleBold(attributes) {
|
||||
function isSpanStyleBold(attributes: any) {
|
||||
const style = attributes.style;
|
||||
if (!style) return false;
|
||||
|
||||
@@ -406,13 +453,13 @@ function isSpanStyleBold(attributes) {
|
||||
}
|
||||
}
|
||||
|
||||
function isSpanStyleItalic(attributes) {
|
||||
function isSpanStyleItalic(attributes: any) {
|
||||
let style = attributes.style;
|
||||
style = style.replace(/\s+/g, '');
|
||||
return (style.toLowerCase().includes('font-style:italic;'));
|
||||
}
|
||||
|
||||
function displaySaxWarning(context, message) {
|
||||
function displaySaxWarning(context: any, message: string) {
|
||||
const line = [];
|
||||
const parser = context ? context._parser : null;
|
||||
if (parser) {
|
||||
@@ -422,31 +469,29 @@ function displaySaxWarning(context, message) {
|
||||
console.warn(line.join(': '));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
function removeSectionParent(section) {
|
||||
if (typeof section === 'string') return section;
|
||||
// function removeSectionParent(section:Section | string) {
|
||||
// if (typeof section === 'string') return section;
|
||||
|
||||
section = { ...section };
|
||||
delete section.parent;
|
||||
// section = { ...section };
|
||||
// delete section.parent;
|
||||
|
||||
section.lines = section.lines.slice();
|
||||
// section.lines = section.lines.slice();
|
||||
|
||||
for (let i = 0; i < section.lines.length; i++) {
|
||||
section.lines[i] = removeSectionParent(section.lines[i]);
|
||||
}
|
||||
// for (let i = 0; i < section.lines.length; i++) {
|
||||
// section.lines[i] = removeSectionParent(section.lines[i]);
|
||||
// }
|
||||
|
||||
return section;
|
||||
}
|
||||
// return section;
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
function printSection(section) {
|
||||
console.info(JSON.stringify(removeSectionParent(section), null, 4));
|
||||
}
|
||||
// function printSection(section:Section) {
|
||||
// console.info(JSON.stringify(removeSectionParent(section), null, 4));
|
||||
// }
|
||||
|
||||
function enexXmlToMdArray(stream, resources) {
|
||||
function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<EnexXmlToMdArrayResult> {
|
||||
const remainingResources = resources.slice();
|
||||
|
||||
const removeRemainingResource = id => {
|
||||
const removeRemainingResource = (id: string) => {
|
||||
for (let i = 0; i < remainingResources.length; i++) {
|
||||
const r = remainingResources[i];
|
||||
if (r.id === id) {
|
||||
@@ -456,7 +501,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const state = {
|
||||
const state: ParserState = {
|
||||
inCode: [],
|
||||
inPre: false,
|
||||
inQuote: false,
|
||||
@@ -470,17 +515,17 @@ function enexXmlToMdArray(stream, resources) {
|
||||
const strict = false;
|
||||
const saxStream = require('@joplin/fork-sax').createStream(strict, options);
|
||||
|
||||
let section = {
|
||||
type: 'text',
|
||||
let section: Section = {
|
||||
type: SectionType.Text,
|
||||
lines: [],
|
||||
parent: null,
|
||||
};
|
||||
|
||||
saxStream.on('error', function(e) {
|
||||
saxStream.on('error', function(e: any) {
|
||||
console.warn(e);
|
||||
});
|
||||
|
||||
const unwrapInnerText = text => {
|
||||
const unwrapInnerText = (text: string) => {
|
||||
const lines = text.split('\n');
|
||||
|
||||
let output = '';
|
||||
@@ -504,14 +549,14 @@ function enexXmlToMdArray(stream, resources) {
|
||||
return output;
|
||||
};
|
||||
|
||||
saxStream.on('text', function(text) {
|
||||
saxStream.on('text', function(text: string) {
|
||||
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
|
||||
|
||||
text = !state.inPre ? unwrapInnerText(text) : text;
|
||||
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
|
||||
});
|
||||
|
||||
saxStream.on('opentag', function(node) {
|
||||
saxStream.on('opentag', function(node: any) {
|
||||
const nodeAttributes = attributeToLowerCase(node);
|
||||
const n = node.name.toLowerCase();
|
||||
const isVisible = !isInvisibleBlock(nodeAttributes);
|
||||
@@ -542,8 +587,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
if (n == 'en-note') {
|
||||
// Start of note
|
||||
} else if (n == 'table') {
|
||||
const newSection = {
|
||||
type: 'table',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Table,
|
||||
lines: [],
|
||||
parent: section,
|
||||
};
|
||||
@@ -568,8 +613,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// return;
|
||||
}
|
||||
|
||||
const newSection = {
|
||||
type: 'tr',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Tr,
|
||||
lines: [],
|
||||
parent: section,
|
||||
isHeader: false,
|
||||
@@ -585,8 +630,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
|
||||
if (n == 'th') section.isHeader = true;
|
||||
|
||||
const newSection = {
|
||||
type: 'td',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Td,
|
||||
lines: [],
|
||||
parent: section,
|
||||
};
|
||||
@@ -599,8 +644,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// return;
|
||||
}
|
||||
|
||||
const newSection = {
|
||||
type: 'caption',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Caption,
|
||||
lines: [],
|
||||
parent: section,
|
||||
};
|
||||
@@ -608,8 +653,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (isInvisibleBlock(nodeAttributes)) {
|
||||
const newSection = {
|
||||
type: 'hidden',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Hidden,
|
||||
lines: [],
|
||||
parent: section,
|
||||
};
|
||||
@@ -650,7 +695,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// Many (most?) img tags don't have no source associated, especially when they were imported from HTML
|
||||
let s = '`;
|
||||
s += `](${markdownUtils.escapeLinkUrl(nodeAttributes.src)})`;
|
||||
section.lines.push(s);
|
||||
}
|
||||
} else if (isAnchor(n)) {
|
||||
@@ -694,8 +739,8 @@ function enexXmlToMdArray(stream, resources) {
|
||||
state.inCode.push(true);
|
||||
state.currentCode = '';
|
||||
|
||||
const newSection = {
|
||||
type: 'code',
|
||||
const newSection: Section = {
|
||||
type: SectionType.Code,
|
||||
lines: [],
|
||||
parent: section,
|
||||
};
|
||||
@@ -802,7 +847,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
}
|
||||
});
|
||||
|
||||
saxStream.on('closetag', function(n) {
|
||||
saxStream.on('closetag', function(n: string) {
|
||||
n = n ? n.toLowerCase() : n;
|
||||
|
||||
const poppedTag = state.tags.pop();
|
||||
@@ -940,7 +985,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// [ Sign in ](https://example.com)
|
||||
// to:
|
||||
// [Sign in](https://example.com)
|
||||
const trimTextStartAndEndSpaces = function(lines) {
|
||||
const trimTextStartAndEndSpaces = function(lines: string[]) {
|
||||
let firstBracketIndex = 0;
|
||||
let foundFirstNonWhite = false;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
@@ -999,14 +1044,14 @@ function enexXmlToMdArray(stream, resources) {
|
||||
resolve({
|
||||
content: section,
|
||||
resources: remainingResources,
|
||||
});
|
||||
} as EnexXmlToMdArrayResult);
|
||||
});
|
||||
|
||||
stream.pipe(saxStream);
|
||||
});
|
||||
}
|
||||
|
||||
function tableHasSubTables(table) {
|
||||
function tableHasSubTables(table: Section) {
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
if (!tr || !tr.lines) continue;
|
||||
@@ -1029,7 +1074,7 @@ function tableHasSubTables(table) {
|
||||
// via Web Clipper. So to handle this, we render all the outer tables as regular text (as if replacing all the <table>, <tr> and <td>
|
||||
// elements by <div>) and only the inner ones, those that don't contain any other tables, are rendered as actual tables. This is generally
|
||||
// the required behaviour since the outer tables are usually for layout and the inner ones are the content.
|
||||
function drawTable(table) {
|
||||
function drawTable(table: Section) {
|
||||
// | First Header | Second Header |
|
||||
// | ------------- | ------------- |
|
||||
// | Content Cell | Content Cell |
|
||||
@@ -1061,7 +1106,7 @@ function drawTable(table) {
|
||||
if (flatRender) {
|
||||
line.push(BLOCK_OPEN);
|
||||
|
||||
let currentCells = [];
|
||||
let currentCells: any[] = [];
|
||||
|
||||
const renderCurrentCells = () => {
|
||||
if (!currentCells.length) return;
|
||||
@@ -1092,7 +1137,7 @@ function drawTable(table) {
|
||||
|
||||
// A cell in a Markdown table cannot have actual new lines so replace
|
||||
// them with <br>, which are supported by the markdown renderers.
|
||||
let cellText = processMdArrayNewLines(td.lines, true);
|
||||
let cellText = processMdArrayNewLines(td.lines);
|
||||
let lines = cellText.split('\n');
|
||||
lines = postProcessMarkdown(lines);
|
||||
cellText = lines.join('\n').replace(/\n+/g, '<br>');
|
||||
@@ -1142,19 +1187,19 @@ function drawTable(table) {
|
||||
lines.push(BLOCK_CLOSE);
|
||||
|
||||
if (caption) {
|
||||
const captionLines = renderLines(caption.lines);
|
||||
const captionLines: any[] = renderLines(caption.lines);
|
||||
lines = lines.concat(captionLines);
|
||||
}
|
||||
|
||||
return flatRender ? lines : lines.join(`<<<<:D>>>>${NEWLINE}<<<<:D>>>>`).split('<<<<:D>>>>');
|
||||
}
|
||||
|
||||
function postProcessMarkdown(lines) {
|
||||
function postProcessMarkdown(lines: string[]) {
|
||||
// After importing HTML, the resulting Markdown often has empty lines at the beginning and end due to
|
||||
// block start/end or elements that were ignored, etc. If these white spaces were intended it's not really
|
||||
// possible to detect it, so simply trim them all so that the result is more deterministic and can be
|
||||
// easily unit tested.
|
||||
const trimEmptyLines = function(lines) {
|
||||
const trimEmptyLines = function(lines: string[]) {
|
||||
while (lines.length) {
|
||||
if (!lines[0].trim()) {
|
||||
lines.splice(0, 1);
|
||||
@@ -1174,7 +1219,7 @@ function postProcessMarkdown(lines) {
|
||||
return lines;
|
||||
};
|
||||
|
||||
function cleanUpSpaces(lines) {
|
||||
function cleanUpSpaces(lines: string[]) {
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -1203,7 +1248,7 @@ function postProcessMarkdown(lines) {
|
||||
|
||||
// A "line" can be some Markdown text, or it can be a section, like a table,
|
||||
// etc. so this function returns an array of strings.
|
||||
function renderLine(line) {
|
||||
function renderLine(line: any) {
|
||||
if (typeof line === 'object' && line.type === 'table') {
|
||||
// A table
|
||||
const table = line;
|
||||
@@ -1227,8 +1272,8 @@ function renderLine(line) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderLines(lines) {
|
||||
let mdLines = [];
|
||||
function renderLines(lines: any[]) {
|
||||
let mdLines: string[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const renderedLines = renderLine(lines[i]);
|
||||
mdLines = mdLines.concat(renderedLines);
|
||||
@@ -1236,9 +1281,9 @@ function renderLines(lines) {
|
||||
return mdLines;
|
||||
}
|
||||
|
||||
async function enexXmlToMd(xmlString, resources, options = {}) {
|
||||
async function enexXmlToMd(xmlString: string, resources: ResourceEntity[]) {
|
||||
const stream = stringToStream(xmlString);
|
||||
const result = await enexXmlToMdArray(stream, resources, options);
|
||||
const result = await enexXmlToMdArray(stream, resources);
|
||||
|
||||
let mdLines = renderLines(result.content.lines);
|
||||
|
||||
@@ -1258,4 +1303,4 @@ async function enexXmlToMd(xmlString, resources, options = {}) {
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
module.exports = { enexXmlToMd, processMdArrayNewLines, NEWLINE, addResourceTag };
|
||||
export { enexXmlToMd, processMdArrayNewLines, NEWLINE, addResourceTag };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -39,43 +39,43 @@ locales['tr_TR'] = require('./tr_TR.json');
|
||||
locales['vi'] = require('./vi.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
locales['zh_TW'] = require('./zh_TW.json');
|
||||
stats['ar'] = {"percentDone":77};
|
||||
stats['eu'] = {"percentDone":33};
|
||||
stats['bs_BA'] = {"percentDone":79};
|
||||
stats['bg_BG'] = {"percentDone":64};
|
||||
stats['ca'] = {"percentDone":92};
|
||||
stats['ar'] = {"percentDone":75};
|
||||
stats['eu'] = {"percentDone":32};
|
||||
stats['bs_BA'] = {"percentDone":77};
|
||||
stats['bg_BG'] = {"percentDone":62};
|
||||
stats['ca'] = {"percentDone":89};
|
||||
stats['hr_HR'] = {"percentDone":26};
|
||||
stats['cs_CZ'] = {"percentDone":96};
|
||||
stats['da_DK'] = {"percentDone":79};
|
||||
stats['de_DE'] = {"percentDone":96};
|
||||
stats['et_EE'] = {"percentDone":63};
|
||||
stats['cs_CZ'] = {"percentDone":93};
|
||||
stats['da_DK'] = {"percentDone":77};
|
||||
stats['de_DE'] = {"percentDone":95};
|
||||
stats['et_EE'] = {"percentDone":61};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":96};
|
||||
stats['eo'] = {"percentDone":36};
|
||||
stats['fi_FI'] = {"percentDone":97};
|
||||
stats['fr_FR'] = {"percentDone":96};
|
||||
stats['gl_ES'] = {"percentDone":42};
|
||||
stats['id_ID'] = {"percentDone":88};
|
||||
stats['it_IT'] = {"percentDone":97};
|
||||
stats['nl_BE'] = {"percentDone":33};
|
||||
stats['nl_NL'] = {"percentDone":96};
|
||||
stats['nb_NO'] = {"percentDone":85};
|
||||
stats['fa'] = {"percentDone":79};
|
||||
stats['pl_PL'] = {"percentDone":95};
|
||||
stats['pt_BR'] = {"percentDone":93};
|
||||
stats['pt_PT'] = {"percentDone":95};
|
||||
stats['ro'] = {"percentDone":74};
|
||||
stats['sl_SI'] = {"percentDone":41};
|
||||
stats['sv'] = {"percentDone":68};
|
||||
stats['th_TH'] = {"percentDone":50};
|
||||
stats['vi'] = {"percentDone":82};
|
||||
stats['tr_TR'] = {"percentDone":94};
|
||||
stats['el_GR'] = {"percentDone":92};
|
||||
stats['ru_RU'] = {"percentDone":92};
|
||||
stats['sr_RS'] = {"percentDone":69};
|
||||
stats['zh_CN'] = {"percentDone":97};
|
||||
stats['zh_TW'] = {"percentDone":91};
|
||||
stats['ja_JP'] = {"percentDone":97};
|
||||
stats['ko'] = {"percentDone":97};
|
||||
stats['es_ES'] = {"percentDone":99};
|
||||
stats['eo'] = {"percentDone":35};
|
||||
stats['fi_FI'] = {"percentDone":94};
|
||||
stats['fr_FR'] = {"percentDone":99};
|
||||
stats['gl_ES'] = {"percentDone":40};
|
||||
stats['id_ID'] = {"percentDone":85};
|
||||
stats['it_IT'] = {"percentDone":96};
|
||||
stats['nl_BE'] = {"percentDone":32};
|
||||
stats['nl_NL'] = {"percentDone":93};
|
||||
stats['nb_NO'] = {"percentDone":82};
|
||||
stats['fa'] = {"percentDone":77};
|
||||
stats['pl_PL'] = {"percentDone":92};
|
||||
stats['pt_BR'] = {"percentDone":91};
|
||||
stats['pt_PT'] = {"percentDone":92};
|
||||
stats['ro'] = {"percentDone":72};
|
||||
stats['sl_SI'] = {"percentDone":40};
|
||||
stats['sv'] = {"percentDone":66};
|
||||
stats['th_TH'] = {"percentDone":49};
|
||||
stats['vi'] = {"percentDone":79};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['el_GR'] = {"percentDone":89};
|
||||
stats['ru_RU'] = {"percentDone":96};
|
||||
stats['sr_RS'] = {"percentDone":67};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":95};
|
||||
stats['ja_JP'] = {"percentDone":96};
|
||||
stats['ko'] = {"percentDone":96};
|
||||
module.exports = { locales: locales, stats: stats };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -141,7 +141,7 @@ class Note extends BaseItem {
|
||||
useAbsolutePaths: false,
|
||||
}, options);
|
||||
|
||||
this.logger().debug('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
|
||||
// this.logger().debug('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
|
||||
|
||||
const resourceIds = await this.linkedResourceIds(body);
|
||||
const Resource = this.getClass('Resource');
|
||||
@@ -161,7 +161,7 @@ class Note extends BaseItem {
|
||||
body = body.replace(new RegExp(`:/${id}`, 'gi'), markdownUtils.escapeLinkUrl(resourcePath));
|
||||
}
|
||||
|
||||
this.logger().debug('replaceResourceInternalToExternalLinks result', body);
|
||||
// this.logger().debug('replaceResourceInternalToExternalLinks result', body);
|
||||
|
||||
return body;
|
||||
}
|
||||
@@ -194,7 +194,7 @@ class Note extends BaseItem {
|
||||
|
||||
pathsToTry = temp;
|
||||
|
||||
this.logger().debug('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry);
|
||||
// this.logger().debug('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry);
|
||||
|
||||
for (const basePath of pathsToTry) {
|
||||
const reStrings = [
|
||||
|
||||
@@ -313,6 +313,17 @@ class Resource extends BaseItem {
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static async createdLocallyCount() {
|
||||
const r = await this.db().selectOne(`
|
||||
SELECT count(*) as total
|
||||
FROM resources
|
||||
WHERE id NOT IN
|
||||
(SELECT resource_id FROM resource_local_states)
|
||||
`);
|
||||
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static fetchStatusToLabel(status) {
|
||||
if (status === Resource.FETCH_STATUS_IDLE) return _('Not downloaded');
|
||||
if (status === Resource.FETCH_STATUS_STARTED) return _('Downloading');
|
||||
|
||||
@@ -239,7 +239,7 @@ class Revision extends BaseItem {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'revision_encrypted') {
|
||||
this.logger().info(`Aborted deletion of old revisions for item ${rev.item_id} because one of the revisions is still encrypted`, error);
|
||||
this.logger().info(`Aborted deletion of old revisions for item "${rev.item_id}" (rev "${rev.id}") because one of the revisions is still encrypted`, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class Setting extends BaseModel {
|
||||
section: 'sync',
|
||||
label: () => _('Synchronisation target'),
|
||||
description: (appType: string) => {
|
||||
return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).');
|
||||
return appType !== 'cli' ? null : _('The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).');
|
||||
},
|
||||
options: () => {
|
||||
return SyncTargetRegistry.idAndLabelPlainObject(platform);
|
||||
|
||||
@@ -6,14 +6,14 @@ export default class ViewController {
|
||||
private pluginId_: string;
|
||||
private store_: any;
|
||||
|
||||
constructor(handle: ViewHandle, pluginId: string, store: any) {
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any) {
|
||||
this.handle_ = handle;
|
||||
this.pluginId_ = pluginId;
|
||||
this.store_ = store;
|
||||
}
|
||||
|
||||
protected get storeView(): any {
|
||||
return this.store_.pluginService.plugins[this.pluginId_].views[this.handle];
|
||||
return this.store_.getState().pluginService.plugins[this.pluginId_].views[this.handle];
|
||||
}
|
||||
|
||||
protected get store(): any {
|
||||
|
||||
@@ -17,13 +17,33 @@ interface CloseResponse {
|
||||
reject: Function;
|
||||
}
|
||||
|
||||
// TODO: Copied from:
|
||||
// packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.ts
|
||||
function findItemByKey(layout: any, key: string): any {
|
||||
if (!layout) throw new Error('Layout cannot be null');
|
||||
|
||||
function recurseFind(item: any): any {
|
||||
if (item.key === key) return item;
|
||||
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
const found = recurseFind(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return recurseFind(layout);
|
||||
}
|
||||
|
||||
export default class WebviewController extends ViewController {
|
||||
|
||||
private baseDir_: string;
|
||||
private messageListener_: Function = null;
|
||||
private closeResponse_: CloseResponse = null;
|
||||
|
||||
constructor(id: string, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
public constructor(id: string, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
super(id, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
|
||||
@@ -91,6 +111,29 @@ export default class WebviewController extends ViewController {
|
||||
this.messageListener_ = callback;
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Specific to panels
|
||||
// ---------------------------------------------
|
||||
|
||||
public async show(show: boolean = true): Promise<void> {
|
||||
this.store.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
|
||||
itemKey: this.handle,
|
||||
propName: 'visible',
|
||||
propValue: show,
|
||||
});
|
||||
}
|
||||
|
||||
public async hide(): Promise<void> {
|
||||
return this.show(false);
|
||||
}
|
||||
|
||||
public get visible(): boolean {
|
||||
const mainLayout = this.store.getState().mainLayout;
|
||||
const item = findItemByKey(mainLayout, this.handle);
|
||||
return item ? item.visible : false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Specific to dialogs
|
||||
// ---------------------------------------------
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class JoplinViewsPanels {
|
||||
private store: any;
|
||||
private plugin: Plugin;
|
||||
|
||||
constructor(plugin: Plugin, store: any) {
|
||||
public constructor(plugin: Plugin, store: any) {
|
||||
this.store = store;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export default class JoplinViewsPanels {
|
||||
/**
|
||||
* Creates a new panel
|
||||
*/
|
||||
async create(id: string): Promise<ViewHandle> {
|
||||
public async create(id: string): Promise<ViewHandle> {
|
||||
if (!id) {
|
||||
this.plugin.deprecationNotice('1.5', 'Creating a view without an ID is deprecated. To fix it, change your call to `joplin.views.panels.create("my-unique-id")`');
|
||||
id = `${this.plugin.viewCount}`;
|
||||
@@ -44,22 +44,43 @@ export default class JoplinViewsPanels {
|
||||
/**
|
||||
* Sets the panel webview HTML
|
||||
*/
|
||||
async setHtml(handle: ViewHandle, html: string) {
|
||||
public async setHtml(handle: ViewHandle, html: string) {
|
||||
return this.controller(handle).html = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and loads a new JS or CSS files into the panel.
|
||||
*/
|
||||
async addScript(handle: ViewHandle, scriptPath: string) {
|
||||
public async addScript(handle: ViewHandle, scriptPath: string) {
|
||||
return this.controller(handle).addScript(scriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
async onMessage(handle: ViewHandle, callback: Function) {
|
||||
public async onMessage(handle: ViewHandle, callback: Function) {
|
||||
return this.controller(handle).onMessage(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
public async show(handle: ViewHandle, show: boolean = true): Promise<void> {
|
||||
await this.controller(handle).show(show);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
public async hide(handle: ViewHandle): Promise<void> {
|
||||
await this.show(handle, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
public async visible(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).visible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -187,8 +187,10 @@ class ReportService {
|
||||
if (status === Resource.FETCH_STATUS_DONE) {
|
||||
const downloadedButEncryptedBlobCount = await Resource.downloadedButEncryptedBlobCount();
|
||||
const downloadedCount = await Resource.downloadStatusCounts(Resource.FETCH_STATUS_DONE);
|
||||
const createdLocallyCount = await Resource.createdLocallyCount();
|
||||
section.body.push(_('%s: %d', _('Downloaded and decrypted'), downloadedCount - downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Downloaded and encrypted'), downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Created locally'), createdLocallyCount));
|
||||
} else {
|
||||
const count = await Resource.downloadStatusCounts(status);
|
||||
section.body.push(_('%s: %d', Resource.fetchStatusToLabel(status), count));
|
||||
|
||||
@@ -24,6 +24,10 @@ module.exports = function(markdownIt) {
|
||||
// url should be normalized at this point, and existing entities are decoded
|
||||
const str = url.trim().toLowerCase();
|
||||
|
||||
if (str.indexOf('data:image/svg+xml,') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return BAD_PROTO_RE.test(str) ? (GOOD_DATA_RE.test(str) ? true : false) : true;
|
||||
};
|
||||
|
||||
|
||||
95
packages/server/package-lock.json
generated
95
packages/server/package-lock.json
generated
@@ -429,6 +429,11 @@
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
|
||||
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -1257,6 +1262,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/highlight.js": {
|
||||
"version": "9.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
|
||||
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/http-assert": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz",
|
||||
@@ -1326,6 +1337,29 @@
|
||||
"@types/koa": "*"
|
||||
}
|
||||
},
|
||||
"@types/linkify-it": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.0.tgz",
|
||||
"integrity": "sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/markdown-it": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.0.tgz",
|
||||
"integrity": "sha512-+RJNprPSIcEUBzj3nx8WYwRsDdAKF6/dG932OleYKbTqBSJ7VvZK0JbPKeEpIYxoniUhgvgyZjO4vlCd4mFTdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/highlight.js": "^9.7.0",
|
||||
"@types/linkify-it": "*",
|
||||
"@types/mdurl": "*"
|
||||
}
|
||||
},
|
||||
"@types/mdurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||
@@ -1552,6 +1586,14 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"arr-diff": {
|
||||
@@ -2439,6 +2481,11 @@
|
||||
"whatwg-url": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.9.8",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.8.tgz",
|
||||
"integrity": "sha512-F42qBtJRa30FKF7XDnOQyNUTsaxDkuaZRj/i7BejSHC34LlLfPoIU4aeopvWfM+m1dJ6/DHKAWLg2ur+pLgq1w=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -2634,6 +2681,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -5544,6 +5596,14 @@
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -5614,6 +5674,30 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz",
|
||||
"integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -7159,12 +7243,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz",
|
||||
@@ -7566,6 +7644,11 @@
|
||||
"integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.2.tgz",
|
||||
|
||||
@@ -12,15 +12,18 @@
|
||||
"watch": "tsc --watch --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"@joplin/lib": "^1.0.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bulma": "^0.9.1",
|
||||
"bulma-prefers-dark": "^0.1.0-beta.0",
|
||||
"dayjs": "^1.9.8",
|
||||
"formidable": "^1.2.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-entities": "^1.3.1",
|
||||
"knex": "^0.19.4",
|
||||
"koa": "^2.8.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"mustache": "^3.1.0",
|
||||
"nanoid": "^2.1.1",
|
||||
"nodemon": "^2.0.6",
|
||||
@@ -35,6 +38,7 @@
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/koa": "^2.0.49",
|
||||
"@types/markdown-it": "^12.0.0",
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/yargs": "^13.0.2",
|
||||
"jest": "^26.6.3",
|
||||
|
||||
0
packages/server/public/css/index/files.css
Normal file
0
packages/server/public/css/index/files.css
Normal file
@@ -25,3 +25,11 @@ input.form-control {
|
||||
.main {
|
||||
padding: 0 3rem;
|
||||
}
|
||||
|
||||
table.table .nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.table .stretch {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// Allows displaying error stack traces with TypeScript file paths
|
||||
require('source-map-support').install();
|
||||
|
||||
import * as Koa from 'koa';
|
||||
import routes from './routes/routes';
|
||||
import { ErrorNotFound } from './utils/errors';
|
||||
import * as fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from './utils/routeUtils';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, baseUrl } from './config';
|
||||
import configDev from './config-dev';
|
||||
@@ -14,9 +13,16 @@ import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
|
||||
import modelFactory from './models/factory';
|
||||
import controllerFactory from './controllers/factory';
|
||||
import { AppContext, Config } from './utils/types';
|
||||
import { AppContext, Config, Env } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import mustacheService, { isView, View } from './services/MustacheService';
|
||||
import routeHandler from './middleware/routeHandler';
|
||||
import notificationHandler from './middleware/notificationHandler';
|
||||
import ownerHandler from './middleware/ownerHandler';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
const env: Env = argv.env as Env || Env.Prod;
|
||||
|
||||
interface Configs {
|
||||
[name: string]: Config;
|
||||
@@ -28,13 +34,6 @@ const configs: Configs = {
|
||||
buildTypes: configBuildTypes,
|
||||
};
|
||||
|
||||
require('source-map-support').install();
|
||||
|
||||
const env: string = argv.env as string || 'prod';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
let appLogger_: LoggerWrapper = null;
|
||||
|
||||
function appLogger(): LoggerWrapper {
|
||||
@@ -46,59 +45,13 @@ function appLogger(): LoggerWrapper {
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx: Koa.Context) => {
|
||||
appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject);
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
appLogger().error(error.httpCode + ': ' + `${ctx.request.method} ${ctx.path}` + ' : ' + error.message);
|
||||
} else {
|
||||
appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
if (env === 'dev' && error.stack) r.stack = error.stack;
|
||||
if (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Note: the order of middlewares is important. For example, ownerHandler
|
||||
// loads the user, which is then used by notificationHandler. And finally
|
||||
// routeHandler uses data from both previous middlewares. It would be good to
|
||||
// layout these dependencies in code but not clear how to do this.
|
||||
app.use(ownerHandler);
|
||||
app.use(notificationHandler);
|
||||
app.use(routeHandler);
|
||||
|
||||
async function main() {
|
||||
const configObject: Config = configs[env];
|
||||
@@ -111,8 +64,8 @@ async function main() {
|
||||
const globalLogger = new Logger();
|
||||
// globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` });
|
||||
globalLogger.addTarget(TargetType.Console, {
|
||||
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
|
||||
formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
|
||||
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
|
||||
formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
|
||||
});
|
||||
Logger.initializeGlobalLogger(globalLogger);
|
||||
|
||||
@@ -150,9 +103,11 @@ async function main() {
|
||||
delete connectionCheckLogInfo.connection;
|
||||
|
||||
appLogger().info('Connection check:', connectionCheckLogInfo);
|
||||
appContext.db = connectionCheck.connection;//
|
||||
appContext.models = modelFactory(appContext.db);
|
||||
appContext.env = env;
|
||||
appContext.db = connectionCheck.connection;
|
||||
appContext.models = modelFactory(appContext.db, baseUrl());
|
||||
appContext.controllers = controllerFactory(appContext.models);
|
||||
appContext.appLogger = appLogger;
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(appContext.db);
|
||||
|
||||
@@ -3,20 +3,20 @@ import configBase from './config-base';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
// database: {
|
||||
// name: 'dev',
|
||||
// client: 'sqlite3',
|
||||
// asyncStackTraces: true,
|
||||
// },
|
||||
database: {
|
||||
client: 'pg',
|
||||
name: 'joplin',
|
||||
user: 'joplin',
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
password: 'joplin',
|
||||
name: 'dev',
|
||||
client: 'sqlite3',
|
||||
asyncStackTraces: true,
|
||||
},
|
||||
// database: {
|
||||
// client: 'pg',
|
||||
// name: 'joplin',
|
||||
// user: 'joplin',
|
||||
// host: 'localhost',
|
||||
// port: 5432,
|
||||
// password: 'joplin',
|
||||
// asyncStackTraces: true,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Session, User } from '../../db';
|
||||
import { checkPassword } from '../../utils/auth';
|
||||
import { Session } from '../../db';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
import BaseController from '../BaseController';
|
||||
@@ -8,12 +7,10 @@ export default class SessionController extends BaseController {
|
||||
|
||||
public async authenticate(email: string, password: string): Promise<Session> {
|
||||
const userModel = this.models.user();
|
||||
const user: User = await userModel.loadByEmail(email);
|
||||
const user = await userModel.login(email, password);
|
||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||
if (!checkPassword(password, user.password)) throw new ErrorForbidden('Invalid username or password');
|
||||
const session: Session = { id: uuidgen(), user_id: user.id };
|
||||
const sessionModel = this.models.session();
|
||||
return sessionModel.save(session, { isNew: true });
|
||||
return this.models.session().save(session, { isNew: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import IndexHomeController from './index/HomeController';
|
||||
import IndexProfileController from './index/ProfileController';
|
||||
import IndexUserController from './index/UserController';
|
||||
import IndexFileController from './index/FileController';
|
||||
import IndexNotificationController from './index/NotificationController';
|
||||
|
||||
export class Controllers {
|
||||
|
||||
@@ -53,6 +54,10 @@ export class Controllers {
|
||||
return new IndexFileController(this.models_);
|
||||
}
|
||||
|
||||
public indexNotifications() {
|
||||
return new IndexNotificationController(this.models_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function(models: Models) {
|
||||
|
||||
@@ -1,44 +1,98 @@
|
||||
import BaseController from '../BaseController';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { Pagination } from '../../models/utils/pagination';
|
||||
import { Pagination, pageMaxSize, PaginationOrder, requestPaginationOrder, PaginationOrderDir, validatePagination, createPaginationLinks } from '../../models/utils/pagination';
|
||||
import { File } from '../../db';
|
||||
import { baseUrl } from '../../config';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import { setQueryParameters } from '../../utils/urlUtils';
|
||||
|
||||
export function makeFilePagination(query: any): Pagination {
|
||||
const limit = Number(query.limit) || pageMaxSize;
|
||||
const order: PaginationOrder[] = requestPaginationOrder(query, 'name', PaginationOrderDir.ASC);
|
||||
order.splice(0, 0, { by: 'is_directory', dir: PaginationOrderDir.DESC });
|
||||
const page: number = 'page' in query ? Number(query.page) : 1;
|
||||
|
||||
const output: Pagination = { limit, order, page };
|
||||
validatePagination(output);
|
||||
return output;
|
||||
}
|
||||
|
||||
export default class FileController extends BaseController {
|
||||
|
||||
public async getIndex(sessionId: string, dirId: string, pagination: Pagination): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const user = await this.initSession(sessionId);
|
||||
const fileModel = this.models.file({ userId: user.id });
|
||||
const parent: File = dirId ? await fileModel.entityFromItemId(dirId) : await fileModel.userRootFile();
|
||||
const paginatedFiles = await fileModel.childrens(parent.id, pagination);
|
||||
public async getIndex(sessionId: string, dirId: string, query: any): Promise<View> {
|
||||
// Query parameters that should be appended to pagination-related URLs
|
||||
const baseUrlQuery: any = {};
|
||||
if (query.limit) baseUrlQuery.limit = query.limit;
|
||||
if (query.order_by) baseUrlQuery.order_by = query.order_by;
|
||||
if (query.order_dir) baseUrlQuery.order_dir = query.order_dir;
|
||||
|
||||
const pagination = makeFilePagination(query);
|
||||
const owner = await this.initSession(sessionId);
|
||||
const fileModel = this.models.file({ userId: owner.id });
|
||||
const root = await fileModel.userRootFile();
|
||||
const parentTemp: File = dirId ? await fileModel.entityFromItemId(dirId) : root;
|
||||
const parent: File = await fileModel.load(parentTemp.id);
|
||||
const paginatedFiles = await fileModel.childrens(parent.id, pagination);
|
||||
const pageCount = Math.ceil((await fileModel.childrenCount(parent.id)) / pagination.limit);
|
||||
|
||||
const parentBaseUrl = await fileModel.fileUrl(parent.id);
|
||||
const paginationLinks = createPaginationLinks(pagination.page, pageCount, setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||
|
||||
async function fileToViewItem(file: File, fileFullPaths: Record<string, string>): Promise<any> {
|
||||
const filePath = fileFullPaths[file.id];
|
||||
|
||||
let url = `${baseUrl()}/files/${filePath}`;
|
||||
if (!file.is_directory) {
|
||||
url += '/content';
|
||||
} else {
|
||||
url = setQueryParameters(url, baseUrlQuery);
|
||||
}
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
url,
|
||||
type: file.is_directory ? 'directory' : 'file',
|
||||
icon: file.is_directory ? 'far fa-folder' : 'far fa-file',
|
||||
timestamp: formatDateTime(file.updated_time),
|
||||
mime: !file.is_directory ? (file.mime_type || 'binary') : '',
|
||||
};
|
||||
}
|
||||
|
||||
const files: any[] = [];
|
||||
|
||||
const fileFullPaths = await fileModel.itemFullPaths(paginatedFiles.items);
|
||||
|
||||
if (parent.id !== root.id) {
|
||||
const p = await fileModel.load(parent.parent_id);
|
||||
files.push({
|
||||
...await fileToViewItem(p, await fileModel.itemFullPaths([p])),
|
||||
icon: 'fas fa-arrow-left',
|
||||
name: '..',
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of paginatedFiles.items) {
|
||||
files.push(await fileToViewItem(file, fileFullPaths));
|
||||
}
|
||||
|
||||
const view: View = defaultView('files');
|
||||
view.content.paginatedFiles = { ...paginatedFiles, items: files };
|
||||
view.content.paginationLinks = paginationLinks;
|
||||
view.content.postUrl = `${baseUrl()}/files`;
|
||||
view.content.parentId = parent.id;
|
||||
view.cssFiles = ['index/files'];
|
||||
view.partials.push('pagination');
|
||||
|
||||
|
||||
const view: View = defaultView('files', owner);
|
||||
view.content.paginatedFiles = paginatedFiles;
|
||||
return view;
|
||||
}
|
||||
|
||||
// public async getOne(sessionId: string, isNew: boolean, userIdOrString: string | User = null, error: any = null): Promise<View> {
|
||||
// const owner = await this.initSession(sessionId);
|
||||
// const userModel = this.models.user({ userId: owner.id });
|
||||
|
||||
// let user: User = {};
|
||||
|
||||
// if (typeof userIdOrString === 'string') {
|
||||
// user = await userModel.load(userIdOrString as string);
|
||||
// } else {
|
||||
// user = userIdOrString as User;
|
||||
// }
|
||||
|
||||
// const view: View = defaultView('user', owner);
|
||||
// view.content.user = user;
|
||||
// view.content.isNew = isNew;
|
||||
// view.content.buttonTitle = isNew ? 'Create user' : 'Update profile';
|
||||
// view.content.error = error;
|
||||
// view.content.postUrl = `${baseUrl()}/users${isNew ? '/new' : `/${user.id}`}`;
|
||||
// view.partials.push('errorBanner');
|
||||
|
||||
// return view;
|
||||
// }
|
||||
public async deleteAll(sessionId: string, dirId: string): Promise<void> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const fileModel = this.models.file({ userId: owner.id });
|
||||
const parent: File = await fileModel.entityFromItemId(dirId, { returnFullEntity: true });
|
||||
await fileModel.deleteChildren(parent.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import defaultView from '../../utils/defaultView';
|
||||
export default class HomeController extends BaseController {
|
||||
|
||||
public async getIndex(sessionId: string): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
return defaultView('home', owner);
|
||||
await this.initSession(sessionId);
|
||||
return defaultView('home');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import BaseController from '../BaseController';
|
||||
import { Notification } from '../../db';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
|
||||
export default class NotificationController extends BaseController {
|
||||
|
||||
public async patchOne(sessionId: string, notificationId: string, notification: Notification): Promise<void> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const model = this.models.notification({ userId: owner.id });
|
||||
const existingNotification = await model.load(notificationId);
|
||||
if (!existingNotification) throw new ErrorNotFound();
|
||||
|
||||
|
||||
console.info('aaaaaaa', notification);
|
||||
const toSave: Notification = {};
|
||||
if ('read' in notification) toSave.read = notification.read;
|
||||
if (!Object.keys(toSave).length) return;
|
||||
|
||||
toSave.id = notificationId;
|
||||
await model.save(toSave);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default class ProfileController extends BaseController {
|
||||
public async getIndex(sessionId: string, user: User = null, error: any = null): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
|
||||
const view: View = defaultView('profile', owner);
|
||||
const view: View = defaultView('profile');
|
||||
view.content.user = user ? user : owner;
|
||||
view.content.error = error;
|
||||
view.partials.push('errorBanner');
|
||||
|
||||
@@ -7,11 +7,11 @@ import { baseUrl } from '../../config';
|
||||
export default class UserController extends BaseController {
|
||||
|
||||
public async getIndex(sessionId: string): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const owner = await this.initSession(sessionId, true);
|
||||
const userModel = this.models.user({ userId: owner.id });
|
||||
const users = await userModel.all();
|
||||
|
||||
const view: View = defaultView('users', owner);
|
||||
const view: View = defaultView('users');
|
||||
view.content.users = users;
|
||||
return view;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default class UserController extends BaseController {
|
||||
user = userIdOrString as User;
|
||||
}
|
||||
|
||||
const view: View = defaultView('user', owner);
|
||||
const view: View = defaultView('user');
|
||||
view.content.user = user;
|
||||
view.content.isNew = isNew;
|
||||
view.content.buttonTitle = isNew ? 'Create user' : 'Update profile';
|
||||
|
||||
@@ -19,6 +19,9 @@ const logger = Logger.create('db');
|
||||
const migrationDir = `${__dirname}/migrations`;
|
||||
const sqliteDbDir = pathUtils.dirname(__dirname);
|
||||
|
||||
export const defaultAdminEmail = 'admin@localhost';
|
||||
export const defaultAdminPassword = 'admin';
|
||||
|
||||
export type DbConnection = Knex;
|
||||
|
||||
export interface DbConfigConnection {
|
||||
@@ -128,6 +131,17 @@ export async function dropTables(db: DbConnection): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function truncateTables(db: DbConnection): Promise<void> {
|
||||
for (const tableName of allTableNames()) {
|
||||
try {
|
||||
await db(tableName).truncate();
|
||||
} catch (error) {
|
||||
if (isNoSuchTableError(error)) continue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNoSuchTableError(error: any): boolean {
|
||||
if (error) {
|
||||
// Postgres error: 42P01: undefined_table
|
||||
@@ -181,6 +195,11 @@ export enum ItemAddressingType {
|
||||
Path,
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Important = 10,
|
||||
Normal = 20,
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
File = 1,
|
||||
User,
|
||||
@@ -261,6 +280,15 @@ export interface ApiClient extends WithDates, WithUuid {
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export interface Notification extends WithDates, WithUuid {
|
||||
owner_id?: Uuid;
|
||||
level?: NotificationLevel;
|
||||
key?: string;
|
||||
message?: string;
|
||||
read?: number;
|
||||
canBeDismissed?: number;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
users: {
|
||||
id: { type: 'string' },
|
||||
@@ -320,5 +348,16 @@ export const databaseSchema: DatabaseTables = {
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
notifications: {
|
||||
id: { type: 'string' },
|
||||
owner_id: { type: 'string' },
|
||||
level: { type: 'number' },
|
||||
key: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
read: { type: 'number' },
|
||||
canBeDismissed: { type: 'number' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
||||
51
packages/server/src/middleware/notificationHandler.ts
Normal file
51
packages/server/src/middleware/notificationHandler.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppContext, KoaNext, NotificationView } from '../utils/types';
|
||||
import { isApiRequest } from '../utils/requestUtils';
|
||||
import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../db';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
ctx.notifications = [];
|
||||
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
|
||||
const user = ctx.owner;
|
||||
if (!user) return next();
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: user.id });
|
||||
|
||||
if (user.is_admin) {
|
||||
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
|
||||
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
'change_admin_password',
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const markdownIt = new MarkdownIt();
|
||||
const notifications = await notificationModel.allUnreadByUserId(user.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
ctx.notifications = views;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
17
packages/server/src/middleware/ownerHandler.ts
Normal file
17
packages/server/src/middleware/ownerHandler.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AppContext, KoaNext } from '../utils/types';
|
||||
import { isApiRequest, contextSessionId } from '../utils/requestUtils';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('loggedInUserHandler');
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
const sessionId = contextSessionId(ctx);
|
||||
ctx.owner = await ctx.models.session().sessionUser(sessionId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
63
packages/server/src/middleware/routeHandler.ts
Normal file
63
packages/server/src/middleware/routeHandler.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import routes from '../routes/routes';
|
||||
import { ErrorNotFound } from '../utils/errors';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from '../utils/routeUtils';
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import mustacheService, { isView, View } from '../services/MustacheService';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject, {
|
||||
notifications: ctx.notifications || [],
|
||||
owner: ctx.owner,
|
||||
});
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
ctx.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
} else {
|
||||
ctx.appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: ctx.env === Env.Dev ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
if (ctx.env === Env.Dev && error.stack) r.stack = error.stack;
|
||||
if (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
import { DbConnection, defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||
import { hashPassword } from '../utils/auth';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
|
||||
@@ -97,8 +97,8 @@ export async function up(db: DbConnection): Promise<any> {
|
||||
|
||||
await db('users').insert({
|
||||
id: adminId,
|
||||
email: 'admin@localhost',
|
||||
password: hashPassword('admin'),
|
||||
email: defaultAdminEmail,
|
||||
password: hashPassword(defaultAdminPassword),
|
||||
full_name: 'Admin',
|
||||
is_admin: 1,
|
||||
updated_time: now,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.string('id', 32).unique().primary().notNullable();
|
||||
table.string('owner_id', 32).notNullable();
|
||||
table.integer('level').notNullable();
|
||||
table.text('key', 'string').notNullable();
|
||||
table.text('message', 'mediumtext').notNullable();
|
||||
table.integer('read').defaultTo(0).notNullable();
|
||||
table.integer('canBeDismissed').defaultTo(1).notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.unique(['owner_id', 'key']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('notifications');
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType } from '../db';
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType, Notification } from '../db';
|
||||
import TransactionHandler from '../utils/TransactionHandler';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';
|
||||
import { Models } from './factory';
|
||||
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[];
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change | Notification;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[] | Notification[];
|
||||
|
||||
export interface ModelOptions {
|
||||
userId?: string;
|
||||
@@ -34,10 +34,12 @@ export default abstract class BaseModel {
|
||||
private db_: DbConnection;
|
||||
private transactionHandler_: TransactionHandler;
|
||||
private modelFactory_: Function;
|
||||
private baseUrl_: string;
|
||||
|
||||
public constructor(db: DbConnection, modelFactory: Function, options: ModelOptions = null) {
|
||||
public constructor(db: DbConnection, modelFactory: Function, baseUrl: string, options: ModelOptions = null) {
|
||||
this.db_ = db;
|
||||
this.modelFactory_ = modelFactory;
|
||||
this.baseUrl_ = baseUrl;
|
||||
this.options_ = Object.assign({}, options);
|
||||
|
||||
this.transactionHandler_ = new TransactionHandler(db);
|
||||
@@ -52,6 +54,10 @@ export default abstract class BaseModel {
|
||||
return this.modelFactory_(db || this.db);
|
||||
}
|
||||
|
||||
protected get baseUrl(): string {
|
||||
return this.baseUrl_;
|
||||
}
|
||||
|
||||
protected get options(): ModelOptions {
|
||||
return this.options_;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user