1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-17 00:33:59 +02:00

Compare commits

...

39 Commits

Author SHA1 Message Date
Laurent Cozic
365ea86787 Plugins: Updated types 2021-01-02 13:28:36 +00:00
Laurent Cozic
638e55aa6e hide show panel 2021-01-02 13:26:47 +00:00
Helmut K. C. Tessarek
0be8cdf760 Update translations 2021-01-01 21:00:35 -05:00
Po-chiang Chao
545940f545 All: Translation: Update zh_TW.po (#4278)
Include new strings and fuzzy translated strings
2021-01-01 19:18:52 -05:00
Laurent Cozic
d58f39823a Doc: Added link to new specs, and added more details to delta sync spec 2021-01-01 13:25:05 +00:00
Laurent Cozic
f9fb1b8a81 Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-01-01 13:04:30 +00:00
Laurent Cozic
56ded0062a All: Add more log info when a revision cannot be deleted due to still-encrypted itel 2021-01-01 13:04:15 +00:00
Laurent Cozic
83b29d7c51 macOS: Fixed paste as text in Rich Text editor 2021-01-01 12:38:17 +00:00
Arda Kılıçdağı
1ec0746263 All: Translation: Update tr_TR.po (#4274)
Hello,

You can find the updated Turkish translations in this pull request.

Also, happy new year! 🎉
2020-12-31 12:08:09 -05:00
Laurent Cozic
568d11bddf Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-12-31 16:04:06 +00:00
Laurent Cozic
97b25ac99d Doc: Add spec for Joplin Server delta sync 2020-12-31 15:58:38 +00:00
Laurent Cozic
7f1d3d8a5d Doc: Added spec for server URL formats 2020-12-31 12:38:22 +00:00
Elaborendum
fde201fbe9 All: Translation: Update es_ES.po (#4260)
* Updated es_ES.po

* Reverting some changes

* Redid translation using current .po file
2020-12-31 00:59:25 -05:00
Laurent Cozic
694a1b4ede Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-12-30 23:53:20 +00:00
Laurent Cozic
20d126b39d Server: Fixed access control on user list and clean up 2020-12-30 23:50:44 +00:00
Helmut K. C. Tessarek
8ca9c3092a Clipper release v1.6.1 2020-12-30 14:50:47 -05:00
Laurent Cozic
d2771029a3 Server: Support for notifications and clean up 2020-12-30 18:35:18 +00:00
Laurent Cozic
4128c53fcf Update French translation 2020-12-30 13:07:41 +00:00
Laurent Cozic
a14410b28c Android release v1.6.4 2020-12-30 12:04:25 +00:00
Laurent Cozic
d1f8520e6e Mobile: Fixed package import issue 2020-12-30 11:51:43 +00:00
Laurent Cozic
d76746b8e4 Android release v1.6.3 2020-12-30 11:05:04 +00:00
Laurent Cozic
89d173b460 Mobile: Add support for Joplin Server 2020-12-30 10:54:00 +00:00
Laurent Cozic
81aba8b8b0 Server: Fixed pagination 2020-12-30 02:24:29 +00:00
Laurent Cozic
f48697572d Server: Fixed file count issue for PostgreSQL 2020-12-29 23:48:34 +00:00
Laurent Cozic
e61e8b7b94 Server: Fixed URLs 2020-12-29 23:22:57 +00:00
Laurent Cozic
1deab7e8d1 Android release v1.6.2 2020-12-29 19:42:30 +00:00
Laurent Cozic
86b28b5ecf Desktop release v1.6.1 2020-12-29 18:30:38 +00:00
Laurent Cozic
938e723434 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-12-29 18:07:02 +00:00
Laurent Cozic
ee2ec28cd4 Server: Add basic file manager 2020-12-29 18:04:57 +00:00
Helmut K. C. Tessarek
9dc505e85b fix typo in source and all translations 2020-12-29 12:10:52 -05:00
Helmut K. C. Tessarek
3df31584af update en_US.po 2020-12-29 11:50:31 -05:00
Zhang YANG
a7e3b381cb All: Translation: Update zh_CN.po (#4263) 2020-12-29 11:23:33 -05:00
Laurent Cozic
70381a233b Merge branch 'release-1.5' into dev 2020-12-29 16:17:18 +00:00
Laurent Cozic
2966fe0df2 Desktop release v1.5.13 2020-12-29 16:14:27 +00:00
Laurent Cozic
143f0b4bc5 Desktop, Cli: Improve support for SVG images when importing ENEX files 2020-12-29 15:58:20 +00:00
Helmut K. C. Tessarek
24ec3b8897 Update translations (for new server code) 2020-12-28 15:17:17 -05:00
Beowulf2
01aa4f9d5e All: Translation: Update de_DE.po (#4258) 2020-12-28 14:39:58 -05:00
Manuel Tassi
520efdcb39 All: Translation: Update it_IT.po (#4256)
* Update it_IT.po

* Update it_IT.po

* Update it_IT.po
2020-12-28 14:39:38 -05:00
Laurent Cozic
34a99f738c linter 2020-12-28 17:26:15 +00:00
170 changed files with 18112 additions and 12725 deletions

View File

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

View File

@@ -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
View File

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

View File

@@ -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 -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [أحمد باشا إبراهيم](mailto:fi_ahmed_bacha@esi.dz) | 77%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 33%
![](https://joplinapp.org/images/flags/country-4x3/ba.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/bg.png) | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 64%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 92%
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [أحمد باشا إبراهيم](mailto:fi_ahmed_bacha@esi.dz) | 75%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 32%
![](https://joplinapp.org/images/flags/country-4x3/ba.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/bg.png) | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 62%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 89%
![](https://joplinapp.org/images/flags/country-4x3/hr.png) | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Hrvoje Mandić](mailto:trbuhom@net.hr) | 26%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ee.png) | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 63%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ee.png) | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 61%
![](https://joplinapp.org/images/flags/country-4x3/gb.png) | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/us.png) | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | 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%
![](https://joplinapp.org/images/flags/esperanto.png) | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 36%
![](https://joplinapp.org/images/flags/country-4x3/fi.png) | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | | 97%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 96%
![](https://joplinapp.org/images/flags/es/galicia.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/id.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 33%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/no.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ir.png) | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 79%
![](https://joplinapp.org/images/flags/country-4x3/pl.png) | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | | 95%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/pt.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 74%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | | 41%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 68%
![](https://joplinapp.org/images/flags/country-4x3/th.png) | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 50%
![](https://joplinapp.org/images/flags/country-4x3/vi.png) | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 82%
![](https://joplinapp.org/images/flags/country-4x3/tr.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/gr.png) | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 92%
![](https://joplinapp.org/images/flags/country-4x3/rs.png) | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 69%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 91%
![](https://joplinapp.org/images/flags/country-4x3/jp.png) | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 97%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | 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%
![](https://joplinapp.org/images/flags/esperanto.png) | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 35%
![](https://joplinapp.org/images/flags/country-4x3/fi.png) | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | | 94%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
![](https://joplinapp.org/images/flags/es/galicia.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/id.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 96%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 32%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/no.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ir.png) | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 77%
![](https://joplinapp.org/images/flags/country-4x3/pl.png) | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | | 92%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/pt.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 72%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | | 40%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 66%
![](https://joplinapp.org/images/flags/country-4x3/th.png) | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 49%
![](https://joplinapp.org/images/flags/country-4x3/vi.png) | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 79%
![](https://joplinapp.org/images/flags/country-4x3/tr.png) | 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%
![](https://joplinapp.org/images/flags/country-4x3/gr.png) | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 89%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 96%
![](https://joplinapp.org/images/flags/country-4x3/rs.png) | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 67%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 99%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 95%
![](https://joplinapp.org/images/flags/country-4x3/jp.png) | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국어 | [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

View 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"/>

View File

@@ -0,0 +1 @@
![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20x='0px'%20y='0px'%20width='16px'%20height='16px'%20viewBox='0%200%2024%2024'%20data-evernote-id='0'%20class='js-evernote-checked'%3e%3cg%20transform='translate%280%2c%200%29'%20data-evernote-id='18'%20class='js-evernote-checked'%3e%3cpolygon%20fill='none'%20stroke='%23343434'%20stroke-width='2'%20stroke-linecap='square'%20stroke-miterlimit='10'%20points='12%2c2.6%2015%2c9%2021.4%2c9%2016.7%2c13.9%2018.6%2c21.4%2012%2c17.6%205.4%2c21.4%207.3%2c13.9%202.6%2c9%209%2c9%20'%20stroke-linejoin='miter'%20data-evernote-id='19'%20class='js-evernote-checked'%3e%3c/polygon%3e%3c/g%3e%3c/svg%3e)

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

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

View File

@@ -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>;
}

View File

@@ -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'",

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.6.0",
"version": "1.6.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@@ -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"

View File

@@ -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"
}

View File

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

View File

@@ -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 };

View File

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

View File

@@ -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>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "1.5.3",
"version": "1.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = '![';
if (nodeAttributes.alt) s += tagAttributeToMdText(nodeAttributes.alt);
s += `](${nodeAttributes.src})`;
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

View File

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

View File

@@ -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 = [

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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 {

View File

@@ -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
// ---------------------------------------------

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
};

View File

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

View File

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

View File

@@ -25,3 +25,11 @@ input.form-control {
.main {
padding: 0 3rem;
}
table.table .nowrap {
white-space: nowrap;
}
table.table .stretch {
width: 100%;
}

View File

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

View File

@@ -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;

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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;
}
}
}

View File

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

View File

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

View File

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