You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
57 Commits
testing_up
...
v2.0.4
Author | SHA1 | Date | |
---|---|---|---|
|
87a5f18c7b | ||
|
1d2a3a97d2 | ||
|
42891e37a1 | ||
|
fe802b8ebc | ||
|
3cb6d4568c | ||
|
a9f0a75d9d | ||
|
07d30eb5d2 | ||
|
8f6a47536c | ||
|
d8d83b236e | ||
|
a355600e76 | ||
|
2a58664735 | ||
|
89bc181072 | ||
|
ab7380a09f | ||
|
f8a26cf8f9 | ||
|
3505a2a973 | ||
|
5f94de0f24 | ||
|
6811ea1eb9 | ||
|
7be59a7435 | ||
|
c0683ca4c3 | ||
|
2b286410f6 | ||
|
907ac7c1f8 | ||
|
8bc27021db | ||
|
41ed66d323 | ||
|
0ef7e98479 | ||
|
161c77cb48 | ||
|
50d17bfb36 | ||
|
ee0f23718b | ||
|
cfe4546a0b | ||
|
f45e0d106f | ||
|
12a66342db | ||
|
f2b17560e6 | ||
|
ba30dce6c8 | ||
|
f5984313be | ||
|
df058352a5 | ||
|
cde25fad92 | ||
|
d89bbc5571 | ||
|
71a7fc015a | ||
|
83cef7a824 | ||
|
f65de0c9eb | ||
|
3edf74e6d2 | ||
|
b01aa7eb45 | ||
|
e59e3aa7d1 | ||
|
51051e0ee0 | ||
|
b20ab19f13 | ||
|
68e79f1573 | ||
|
ed8ee67048 | ||
|
68b516998d | ||
|
0fa7a66fb6 | ||
|
13f39b9bd5 | ||
|
013d37bd09 | ||
|
4760e5e8ba | ||
|
8930dac40e | ||
|
3f0586ef63 | ||
|
e94503abbe | ||
|
f8253cc2f0 | ||
|
2806aa1b19 | ||
|
8f57e07279 |
@@ -83,6 +83,9 @@ packages/app-cli/tests/MdToHtml.js.map
|
||||
packages/app-cli/tests/MdToMd.d.ts
|
||||
packages/app-cli/tests/MdToMd.js
|
||||
packages/app-cli/tests/MdToMd.js.map
|
||||
packages/app-cli/tests/services/keychain/KeychainService.d.ts
|
||||
packages/app-cli/tests/services/keychain/KeychainService.js
|
||||
packages/app-cli/tests/services/keychain/KeychainService.js.map
|
||||
packages/app-cli/tests/services/plugins/PluginService.d.ts
|
||||
packages/app-cli/tests/services/plugins/PluginService.js
|
||||
packages/app-cli/tests/services/plugins/PluginService.js.map
|
||||
@@ -101,6 +104,9 @@ packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -1166,9 +1172,6 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
|
||||
packages/lib/services/keychain/keychainService.test.d.ts
|
||||
packages/lib/services/keychain/keychainService.test.js
|
||||
packages/lib/services/keychain/keychainService.test.js.map
|
||||
packages/lib/services/plugins/BasePluginRunner.d.ts
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js.map
|
||||
@@ -1472,6 +1475,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/plugin-repo-cli/commands/updateRelease.d.ts
|
||||
packages/plugin-repo-cli/commands/updateRelease.js
|
||||
packages/plugin-repo-cli/commands/updateRelease.js.map
|
||||
packages/plugin-repo-cli/index.d.ts
|
||||
packages/plugin-repo-cli/index.js
|
||||
packages/plugin-repo-cli/index.js.map
|
||||
@@ -1610,4 +1616,7 @@ packages/tools/release-server.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
packages/tools/update-readme-sponsors.d.ts
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/update-readme-sponsors.js.map
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@@ -69,6 +69,9 @@ packages/app-cli/tests/MdToHtml.js.map
|
||||
packages/app-cli/tests/MdToMd.d.ts
|
||||
packages/app-cli/tests/MdToMd.js
|
||||
packages/app-cli/tests/MdToMd.js.map
|
||||
packages/app-cli/tests/services/keychain/KeychainService.d.ts
|
||||
packages/app-cli/tests/services/keychain/KeychainService.js
|
||||
packages/app-cli/tests/services/keychain/KeychainService.js.map
|
||||
packages/app-cli/tests/services/plugins/PluginService.d.ts
|
||||
packages/app-cli/tests/services/plugins/PluginService.js
|
||||
packages/app-cli/tests/services/plugins/PluginService.js.map
|
||||
@@ -87,6 +90,9 @@ packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -1152,9 +1158,6 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
|
||||
packages/lib/services/keychain/keychainService.test.d.ts
|
||||
packages/lib/services/keychain/keychainService.test.js
|
||||
packages/lib/services/keychain/keychainService.test.js.map
|
||||
packages/lib/services/plugins/BasePluginRunner.d.ts
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js.map
|
||||
@@ -1458,6 +1461,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/plugin-repo-cli/commands/updateRelease.d.ts
|
||||
packages/plugin-repo-cli/commands/updateRelease.js
|
||||
packages/plugin-repo-cli/commands/updateRelease.js.map
|
||||
packages/plugin-repo-cli/index.d.ts
|
||||
packages/plugin-repo-cli/index.js
|
||||
packages/plugin-repo-cli/index.js.map
|
||||
@@ -1596,4 +1602,7 @@ packages/tools/release-server.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
packages/tools/update-readme-sponsors.d.ts
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/update-readme-sponsors.js.map
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
2
BUILD.md
2
BUILD.md
@@ -64,7 +64,7 @@ Normally the **bundler** should start automatically with the application. If it
|
||||
npm install
|
||||
npm run watch # To watch for changes
|
||||
|
||||
To test the extension please refer to the relevant pages for each browser: [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#Trying_it_out) / [Chrome](https://developer.chrome.com/extensions/faq#faq-dev-01). Please note that the extension in dev mode will only connect to a dev instance of the desktop app (and vice-versa).
|
||||
To test the extension please refer to the relevant pages for each browser: [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#Trying_it_out) / [Chrome](https://developer.chrome.com/docs/extensions/mv3/getstarted/). Please note that the extension in dev mode will only connect to a dev instance of the desktop app (and vice-versa).
|
||||
|
||||
## Watching files
|
||||
|
||||
|
218
README.md
218
README.md
@@ -68,13 +68,15 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
|
||||
* * *
|
||||
|
||||
| | | |
|
||||
| :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/6979755?s=96&v=4"/></br>[Devon Zuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[小西 孝宗](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[Alexander van der Berg](https://github.com/avanderberg)
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[Frank Bloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[Thomas Broussard](https://github.com/thomasbroussard)
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[Brandon Johnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars1.githubusercontent.com/u/3061769?s=96&v=4"/></br>[@cnagy](https://github.com/c-nagy) | <img width="50" src="https://avatars3.githubusercontent.com/u/53228972?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl)
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m)
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars3.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jess Sullivan](https://github.com/jesssullivan)
|
||||
<!-- SPONSORS -->
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/3061769?s=96&v=4"/></br>[c-nagy](https://github.com/c-nagy) | <img width="50" src="https://avatars2.githubusercontent.com/u/70780798?s=96&v=4"/></br>[cabottech](https://github.com/cabottech) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | <img width="50" src="https://avatars2.githubusercontent.com/u/53228972?s=96&v=4"/></br>[wasteisobscene](https://github.com/wasteisobscene) | |
|
||||
<!-- SPONSORS -->
|
||||
|
||||
<!-- TOC -->
|
||||
# Table of contents
|
||||
@@ -511,47 +513,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
 | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 96%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 30%
|
||||
 | 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) | 75%
|
||||
 | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 58%
|
||||
 | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 83%
|
||||
 | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
|
||||
 | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 86%
|
||||
 | 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: | 96%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 57%
|
||||
 | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
 | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
 | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 94%
|
||||
 | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 33%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 94%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
|
||||
 | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 38%
|
||||
 | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 93%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 94%
|
||||
 | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Szőke Sándor](mailto:mail@szokesandor.hu) | 88%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 92%
|
||||
 | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 95%
|
||||
 | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 76%
|
||||
 | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 71%
|
||||
 | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 94%
|
||||
 | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 94%
|
||||
 | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 94%
|
||||
 | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 66%
|
||||
 | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 96%
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 61%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 45%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
 | 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%
|
||||
 | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 94%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 97%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 94%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 71%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [Yang Zhang](mailto:zyangmath@gmail.com) | 94%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 92%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 30%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | 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) | 74%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 58%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 82%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | 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: | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 56%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | 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) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 32%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 98%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 38%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alberto Pasqualetto](mailto:39854348+albertopasqualetto@users.) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Szőke Sándor](mailto:mail@szokesandor.hu) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 71%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 66%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 61%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 45%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | 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) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 71%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 98%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
@@ -561,52 +563,80 @@ Thank you to everyone who've contributed to Joplin's source code!
|
||||
<!-- CONTRIBUTORS-TABLE-AUTO-GENERATED -->
|
||||
| | | | | |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1285584?v=4"/></br>[laurent22](https://api.github.com/users/laurent22) | <img width="50" src="https://avatars3.githubusercontent.com/u/223439?v=4"/></br>[tessus](https://api.github.com/users/tessus) | <img width="50" src="https://avatars0.githubusercontent.com/u/1732810?v=4"/></br>[mic704b](https://api.github.com/users/mic704b) | <img width="50" src="https://avatars3.githubusercontent.com/u/2179547?v=4"/></br>[CalebJohn](https://api.github.com/users/CalebJohn) | <img width="50" src="https://avatars1.githubusercontent.com/u/3542031?v=4"/></br>[PackElend](https://api.github.com/users/PackElend) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/4553672?v=4"/></br>[tanrax](https://api.github.com/users/tanrax) | <img width="50" src="https://avatars0.githubusercontent.com/u/8701534?v=4"/></br>[rtmkrlv](https://api.github.com/users/rtmkrlv) | <img width="50" src="https://avatars3.githubusercontent.com/u/10997189?v=4"/></br>[fmrtn](https://api.github.com/users/fmrtn) | <img width="50" src="https://avatars1.githubusercontent.com/u/29672555?v=4"/></br>[genneko](https://api.github.com/users/genneko) | <img width="50" src="https://avatars1.githubusercontent.com/u/6979755?v=4"/></br>[devonzuegel](https://api.github.com/users/devonzuegel) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/16101778?v=4"/></br>[gabcoh](https://api.github.com/users/gabcoh) | <img width="50" src="https://avatars3.githubusercontent.com/u/10927304?v=4"/></br>[matsest](https://api.github.com/users/matsest) | <img width="50" src="https://avatars0.githubusercontent.com/u/6319051?v=4"/></br>[abonte](https://api.github.com/users/abonte) | <img width="50" src="https://avatars2.githubusercontent.com/u/1685517?v=4"/></br>[Abijeet](https://api.github.com/users/Abijeet) | <img width="50" src="https://avatars0.githubusercontent.com/u/27751740?v=4"/></br>[ishantgupta777](https://api.github.com/users/ishantgupta777) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/208212?v=4"/></br>[foxmask](https://api.github.com/users/foxmask) | <img width="50" src="https://avatars2.githubusercontent.com/u/6557454?v=4"/></br>[innocuo](https://api.github.com/users/innocuo) | <img width="50" src="https://avatars1.githubusercontent.com/u/26695184?v=4"/></br>[anjulalk](https://api.github.com/users/anjulalk) | <img width="50" src="https://avatars1.githubusercontent.com/u/44024553?v=4"/></br>[rabeehrz](https://api.github.com/users/rabeehrz) | <img width="50" src="https://avatars0.githubusercontent.com/u/35633575?v=4"/></br>[coderrsid](https://api.github.com/users/coderrsid) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/4237724?v=4"/></br>[alexdevero](https://api.github.com/users/alexdevero) | <img width="50" src="https://avatars3.githubusercontent.com/u/35904727?v=4"/></br>[Runo-saduwa](https://api.github.com/users/Runo-saduwa) | <img width="50" src="https://avatars2.githubusercontent.com/u/5365582?v=4"/></br>[marcosvega91](https://api.github.com/users/marcosvega91) | <img width="50" src="https://avatars3.githubusercontent.com/u/37639389?v=4"/></br>[petrz12](https://api.github.com/users/petrz12) | <img width="50" src="https://avatars0.githubusercontent.com/u/3194829?v=4"/></br>[moltenform](https://api.github.com/users/moltenform) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/5199995?v=4"/></br>[zuphilip](https://api.github.com/users/zuphilip) | <img width="50" src="https://avatars1.githubusercontent.com/u/1904967?v=4"/></br>[readingsnail](https://api.github.com/users/readingsnail) | <img width="50" src="https://avatars0.githubusercontent.com/u/3985557?v=4"/></br>[XarisA](https://api.github.com/users/XarisA) | <img width="50" src="https://avatars2.githubusercontent.com/u/4245227?v=4"/></br>[zblesk](https://api.github.com/users/zblesk) | <img width="50" src="https://avatars2.githubusercontent.com/u/31567272?v=4"/></br>[0ndrey](https://api.github.com/users/0ndrey) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/12906090?v=4"/></br>[amitsin6h](https://api.github.com/users/amitsin6h) | <img width="50" src="https://avatars3.githubusercontent.com/u/23281486?v=4"/></br>[martonpaulo](https://api.github.com/users/martonpaulo) | <img width="50" src="https://avatars3.githubusercontent.com/u/4497566?v=4"/></br>[rccavalcanti](https://api.github.com/users/rccavalcanti) | <img width="50" src="https://avatars0.githubusercontent.com/u/54268438?v=4"/></br>[Rahulm2310](https://api.github.com/users/Rahulm2310) | <img width="50" src="https://avatars0.githubusercontent.com/u/559346?v=4"/></br>[metbril](https://api.github.com/users/metbril) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1540054?v=4"/></br>[ShaneKilkelly](https://api.github.com/users/ShaneKilkelly) | <img width="50" src="https://avatars1.githubusercontent.com/u/6734573?v=4"/></br>[stweil](https://api.github.com/users/stweil) | <img width="50" src="https://avatars3.githubusercontent.com/u/937861?v=4"/></br>[archont00](https://api.github.com/users/archont00) | <img width="50" src="https://avatars3.githubusercontent.com/u/32770029?v=4"/></br>[bradmcl](https://api.github.com/users/bradmcl) | <img width="50" src="https://avatars1.githubusercontent.com/u/22592201?v=4"/></br>[tfinnberg](https://api.github.com/users/tfinnberg) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/3870964?v=4"/></br>[marcushill](https://api.github.com/users/marcushill) | <img width="50" src="https://avatars3.githubusercontent.com/u/102242?v=4"/></br>[nathanleiby](https://api.github.com/users/nathanleiby) | <img width="50" src="https://avatars0.githubusercontent.com/u/226708?v=4"/></br>[RaphaelKimmig](https://api.github.com/users/RaphaelKimmig) | <img width="50" src="https://avatars0.githubusercontent.com/u/17768566?v=4"/></br>[RenatoXSR](https://api.github.com/users/RenatoXSR) | <img width="50" src="https://avatars1.githubusercontent.com/u/36303913?v=4"/></br>[sensor-freak](https://api.github.com/users/sensor-freak) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/2063957?v=4"/></br>[Ardakilic](https://api.github.com/users/Ardakilic) | <img width="50" src="https://avatars3.githubusercontent.com/u/21161146?v=4"/></br>[BartBucknill](https://api.github.com/users/BartBucknill) | <img width="50" src="https://avatars3.githubusercontent.com/u/2494769?v=4"/></br>[mrwulf](https://api.github.com/users/mrwulf) | <img width="50" src="https://avatars2.githubusercontent.com/u/560571?v=4"/></br>[chrisb86](https://api.github.com/users/chrisb86) | <img width="50" src="https://avatars3.githubusercontent.com/u/1686759?v=4"/></br>[chrmoritz](https://api.github.com/users/chrmoritz) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/5001259?v=4"/></br>[ethan42411](https://api.github.com/users/ethan42411) | <img width="50" src="https://avatars2.githubusercontent.com/u/2733783?v=4"/></br>[JOJ0](https://api.github.com/users/JOJ0) | <img width="50" src="https://avatars2.githubusercontent.com/u/3140223?v=4"/></br>[jdrobertso](https://api.github.com/users/jdrobertso) | <img width="50" src="https://avatars2.githubusercontent.com/u/339645?v=4"/></br>[jmontane](https://api.github.com/users/jmontane) | <img width="50" src="https://avatars2.githubusercontent.com/u/4168339?v=4"/></br>[solariz](https://api.github.com/users/solariz) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/390889?v=4"/></br>[mmahmoudian](https://api.github.com/users/mmahmoudian) | <img width="50" src="https://avatars1.githubusercontent.com/u/25288?v=4"/></br>[maicki](https://api.github.com/users/maicki) | <img width="50" src="https://avatars3.githubusercontent.com/u/2136373?v=4"/></br>[mjjzf](https://api.github.com/users/mjjzf) | <img width="50" src="https://avatars3.githubusercontent.com/u/30305957?v=4"/></br>[naviji](https://api.github.com/users/naviji) | <img width="50" src="https://avatars3.githubusercontent.com/u/27608187?v=4"/></br>[rt-oliveira](https://api.github.com/users/rt-oliveira) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/54576074?v=4"/></br>[Rishgod](https://api.github.com/users/Rishgod) | <img width="50" src="https://avatars0.githubusercontent.com/u/2486806?v=4"/></br>[sebastienjust](https://api.github.com/users/sebastienjust) | <img width="50" src="https://avatars2.githubusercontent.com/u/28362310?v=4"/></br>[sealch](https://api.github.com/users/sealch) | <img width="50" src="https://avatars1.githubusercontent.com/u/34258070?v=4"/></br>[StarFang208](https://api.github.com/users/StarFang208) | <img width="50" src="https://avatars2.githubusercontent.com/u/1782292?v=4"/></br>[SubodhDahal](https://api.github.com/users/SubodhDahal) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/5912371?v=4"/></br>[TobiasDev](https://api.github.com/users/TobiasDev) | <img width="50" src="https://avatars2.githubusercontent.com/u/692072?v=4"/></br>[conyx](https://api.github.com/users/conyx) | <img width="50" src="https://avatars2.githubusercontent.com/u/5730052?v=4"/></br>[vsimkus](https://api.github.com/users/vsimkus) | <img width="50" src="https://avatars1.githubusercontent.com/u/4079047?v=4"/></br>[Zorbeyd](https://api.github.com/users/Zorbeyd) | <img width="50" src="https://avatars3.githubusercontent.com/u/5077221?v=4"/></br>[axq](https://api.github.com/users/axq) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/8808502?v=4"/></br>[barbowza](https://api.github.com/users/barbowza) | <img width="50" src="https://avatars1.githubusercontent.com/u/4316805?v=4"/></br>[lightray22](https://api.github.com/users/lightray22) | <img width="50" src="https://avatars0.githubusercontent.com/u/17399340?v=4"/></br>[pf-siedler](https://api.github.com/users/pf-siedler) | <img width="50" src="https://avatars1.githubusercontent.com/u/17232523?v=4"/></br>[ruuti](https://api.github.com/users/ruuti) | <img width="50" src="https://avatars2.githubusercontent.com/u/23638148?v=4"/></br>[s1nceri7y](https://api.github.com/users/s1nceri7y) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/10117386?v=4"/></br>[kornava](https://api.github.com/users/kornava) | <img width="50" src="https://avatars1.githubusercontent.com/u/7471938?v=4"/></br>[ShuiHuo](https://api.github.com/users/ShuiHuo) | <img width="50" src="https://avatars2.githubusercontent.com/u/11596277?v=4"/></br>[ikunya](https://api.github.com/users/ikunya) | <img width="50" src="https://avatars3.githubusercontent.com/u/59133880?v=4"/></br>[bedwardly-down](https://api.github.com/users/bedwardly-down) | <img width="50" src="https://avatars2.githubusercontent.com/u/47456195?v=4"/></br>[hexclover](https://api.github.com/users/hexclover) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/45535789?v=4"/></br>[2jaeyeol](https://api.github.com/users/2jaeyeol) | <img width="50" src="https://avatars1.githubusercontent.com/u/15862474?v=4"/></br>[aaronxn](https://api.github.com/users/aaronxn) | <img width="50" src="https://avatars1.githubusercontent.com/u/3660978?v=4"/></br>[alanfortlink](https://api.github.com/users/alanfortlink) | <img width="50" src="https://avatars3.githubusercontent.com/u/14836659?v=4"/></br>[apankratov](https://api.github.com/users/apankratov) | <img width="50" src="https://avatars1.githubusercontent.com/u/7045739?v=4"/></br>[teterkin](https://api.github.com/users/teterkin) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/41290751?v=4"/></br>[serenitatis](https://api.github.com/users/serenitatis) | <img width="50" src="https://avatars2.githubusercontent.com/u/4408379?v=4"/></br>[lex111](https://api.github.com/users/lex111) | <img width="50" src="https://avatars2.githubusercontent.com/u/5417051?v=4"/></br>[tekdel](https://api.github.com/users/tekdel) | <img width="50" src="https://avatars1.githubusercontent.com/u/498326?v=4"/></br>[Shaxine](https://api.github.com/users/Shaxine) | <img width="50" src="https://avatars0.githubusercontent.com/u/201215?v=4"/></br>[assimd](https://api.github.com/users/assimd) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/42698687?v=4"/></br>[baymoe](https://api.github.com/users/baymoe) | <img width="50" src="https://avatars2.githubusercontent.com/u/7034200?v=4"/></br>[bimlas](https://api.github.com/users/bimlas) | <img width="50" src="https://avatars0.githubusercontent.com/u/16287077?v=4"/></br>[carlbordum](https://api.github.com/users/carlbordum) | <img width="50" src="https://avatars0.githubusercontent.com/u/105843?v=4"/></br>[chaifeng](https://api.github.com/users/chaifeng) | <img width="50" src="https://avatars2.githubusercontent.com/u/549349?v=4"/></br>[charles-e](https://api.github.com/users/charles-e) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/2348463?v=4"/></br>[Techwolf12](https://api.github.com/users/Techwolf12) | <img width="50" src="https://avatars0.githubusercontent.com/u/2282880?v=4"/></br>[cloudtrends](https://api.github.com/users/cloudtrends) | <img width="50" src="https://avatars2.githubusercontent.com/u/1044056?v=4"/></br>[daniellandau](https://api.github.com/users/daniellandau) | <img width="50" src="https://avatars2.githubusercontent.com/u/26189247?v=4"/></br>[daukadolt](https://api.github.com/users/daukadolt) | <img width="50" src="https://avatars2.githubusercontent.com/u/28535750?v=4"/></br>[NeverMendel](https://api.github.com/users/NeverMendel) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/11378282?v=4"/></br>[diego-betto](https://api.github.com/users/diego-betto) | <img width="50" src="https://avatars0.githubusercontent.com/u/215270?v=4"/></br>[erdody](https://api.github.com/users/erdody) | <img width="50" src="https://avatars0.githubusercontent.com/u/10371667?v=4"/></br>[domgoodwin](https://api.github.com/users/domgoodwin) | <img width="50" src="https://avatars3.githubusercontent.com/u/72066?v=4"/></br>[b4mboo](https://api.github.com/users/b4mboo) | <img width="50" src="https://avatars0.githubusercontent.com/u/5131923?v=4"/></br>[donbowman](https://api.github.com/users/donbowman) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/47756?v=4"/></br>[dflock](https://api.github.com/users/dflock) | <img width="50" src="https://avatars0.githubusercontent.com/u/7990534?v=4"/></br>[drobilica](https://api.github.com/users/drobilica) | <img width="50" src="https://avatars3.githubusercontent.com/u/1962738?v=4"/></br>[einverne](https://api.github.com/users/einverne) | <img width="50" src="https://avatars0.githubusercontent.com/u/628474?v=4"/></br>[Atalanttore](https://api.github.com/users/Atalanttore) | <img width="50" src="https://avatars1.githubusercontent.com/u/16492558?v=4"/></br>[eodeluga](https://api.github.com/users/eodeluga) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/3057302?v=4"/></br>[fer22f](https://api.github.com/users/fer22f) | <img width="50" src="https://avatars0.githubusercontent.com/u/43272148?v=4"/></br>[fpindado](https://api.github.com/users/fpindado) | <img width="50" src="https://avatars2.githubusercontent.com/u/1714374?v=4"/></br>[FleischKarussel](https://api.github.com/users/FleischKarussel) | <img width="50" src="https://avatars1.githubusercontent.com/u/18525376?v=4"/></br>[talkdirty](https://api.github.com/users/talkdirty) | <img width="50" src="https://avatars0.githubusercontent.com/u/6190183?v=4"/></br>[gmag11](https://api.github.com/users/gmag11) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/24235344?v=4"/></br>[guiemi](https://api.github.com/users/guiemi) | <img width="50" src="https://avatars2.githubusercontent.com/u/2257024?v=4"/></br>[gusbemacbe](https://api.github.com/users/gusbemacbe) | <img width="50" src="https://avatars0.githubusercontent.com/u/18524580?v=4"/></br>[Fvbor](https://api.github.com/users/Fvbor) | <img width="50" src="https://avatars0.githubusercontent.com/u/22606250?v=4"/></br>[bennetthanna](https://api.github.com/users/bennetthanna) | <img width="50" src="https://avatars3.githubusercontent.com/u/3379379?v=4"/></br>[sczhg](https://api.github.com/users/sczhg) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/1716229?v=4"/></br>[Vistaus](https://api.github.com/users/Vistaus) | <img width="50" src="https://avatars1.githubusercontent.com/u/19862172?v=4"/></br>[iahmedbacha](https://api.github.com/users/iahmedbacha) | <img width="50" src="https://avatars0.githubusercontent.com/u/1533624?v=4"/></br>[IrvinDominin](https://api.github.com/users/IrvinDominin) | <img width="50" src="https://avatars3.githubusercontent.com/u/33200024?v=4"/></br>[ishammahajan](https://api.github.com/users/ishammahajan) | <img width="50" src="https://avatars0.githubusercontent.com/u/19985741?v=4"/></br>[JRaiden16](https://api.github.com/users/JRaiden16) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/11466782?v=4"/></br>[jacobherrington](https://api.github.com/users/jacobherrington) | <img width="50" src="https://avatars2.githubusercontent.com/u/9365179?v=4"/></br>[jamesadjinwa](https://api.github.com/users/jamesadjinwa) | <img width="50" src="https://avatars1.githubusercontent.com/u/4995433?v=4"/></br>[jaredcrowe](https://api.github.com/users/jaredcrowe) | <img width="50" src="https://avatars3.githubusercontent.com/u/4374338?v=4"/></br>[potatogim](https://api.github.com/users/potatogim) | <img width="50" src="https://avatars0.githubusercontent.com/u/163555?v=4"/></br>[JoelRSimpson](https://api.github.com/users/JoelRSimpson) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/6965062?v=4"/></br>[joeltaylor](https://api.github.com/users/joeltaylor) | <img width="50" src="https://avatars3.githubusercontent.com/u/242107?v=4"/></br>[exic](https://api.github.com/users/exic) | <img width="50" src="https://avatars1.githubusercontent.com/u/23194385?v=4"/></br>[jony0008](https://api.github.com/users/jony0008) | <img width="50" src="https://avatars1.githubusercontent.com/u/6048003?v=4"/></br>[joybinchen](https://api.github.com/users/joybinchen) | <img width="50" src="https://avatars1.githubusercontent.com/u/1560189?v=4"/></br>[y-usuzumi](https://api.github.com/users/y-usuzumi) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1660460?v=4"/></br>[xuhcc](https://api.github.com/users/xuhcc) | <img width="50" src="https://avatars0.githubusercontent.com/u/16933735?v=4"/></br>[kirtanprht](https://api.github.com/users/kirtanprht) | <img width="50" src="https://avatars3.githubusercontent.com/u/7824233?v=4"/></br>[kklas](https://api.github.com/users/kklas) | <img width="50" src="https://avatars1.githubusercontent.com/u/8622992?v=4"/></br>[xmlangel](https://api.github.com/users/xmlangel) | <img width="50" src="https://avatars0.githubusercontent.com/u/1055100?v=4"/></br>[troilus](https://api.github.com/users/troilus) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/50335724?v=4"/></br>[Lorinson](https://api.github.com/users/Lorinson) | <img width="50" src="https://avatars2.githubusercontent.com/u/2599210?v=4"/></br>[lboullo0](https://api.github.com/users/lboullo0) | <img width="50" src="https://avatars1.githubusercontent.com/u/1562062?v=4"/></br>[dbinary](https://api.github.com/users/dbinary) | <img width="50" src="https://avatars3.githubusercontent.com/u/5699725?v=4"/></br>[mvonmaltitz](https://api.github.com/users/mvonmaltitz) | <img width="50" src="https://avatars3.githubusercontent.com/u/11036464?v=4"/></br>[mlkood](https://api.github.com/users/mlkood) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/5788516?v=4"/></br>[Marmo](https://api.github.com/users/Marmo) | <img width="50" src="https://avatars0.githubusercontent.com/u/640949?v=4"/></br>[freaktechnik](https://api.github.com/users/freaktechnik) | <img width="50" src="https://avatars2.githubusercontent.com/u/12831489?v=4"/></br>[mgroth0](https://api.github.com/users/mgroth0) | <img width="50" src="https://avatars0.githubusercontent.com/u/21796?v=4"/></br>[silentmatt](https://api.github.com/users/silentmatt) | <img width="50" src="https://avatars0.githubusercontent.com/u/51273874?v=4"/></br>[MichipX](https://api.github.com/users/MichipX) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/53177864?v=4"/></br>[MrTraduttore](https://api.github.com/users/MrTraduttore) | <img width="50" src="https://avatars3.githubusercontent.com/u/9076687?v=4"/></br>[NJannasch](https://api.github.com/users/NJannasch) | <img width="50" src="https://avatars2.githubusercontent.com/u/12369770?v=4"/></br>[Ouvill](https://api.github.com/users/Ouvill) | <img width="50" src="https://avatars3.githubusercontent.com/u/43815417?v=4"/></br>[shorty2380](https://api.github.com/users/shorty2380) | <img width="50" src="https://avatars0.githubusercontent.com/u/19418601?v=4"/></br>[Rakleed](https://api.github.com/users/Rakleed) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/6306608?v=4"/></br>[Diadlo](https://api.github.com/users/Diadlo) | <img width="50" src="https://avatars1.githubusercontent.com/u/13197246?v=4"/></br>[R-L-T-Y](https://api.github.com/users/R-L-T-Y) | <img width="50" src="https://avatars2.githubusercontent.com/u/42652941?v=4"/></br>[rajprakash00](https://api.github.com/users/rajprakash00) | <img width="50" src="https://avatars0.githubusercontent.com/u/54888685?v=4"/></br>[RedDocMD](https://api.github.com/users/RedDocMD) | <img width="50" src="https://avatars2.githubusercontent.com/u/17312341?v=4"/></br>[reinhart1010](https://api.github.com/users/reinhart1010) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/744655?v=4"/></br>[ruzaq](https://api.github.com/users/ruzaq) | <img width="50" src="https://avatars0.githubusercontent.com/u/19328605?v=4"/></br>[SamuelBlickle](https://api.github.com/users/SamuelBlickle) | <img width="50" src="https://avatars1.githubusercontent.com/u/1776?v=4"/></br>[bronson](https://api.github.com/users/bronson) | <img width="50" src="https://avatars0.githubusercontent.com/u/24606935?v=4"/></br>[semperor](https://api.github.com/users/semperor) | <img width="50" src="https://avatars0.githubusercontent.com/u/7091080?v=4"/></br>[sinkuu](https://api.github.com/users/sinkuu) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/9937486?v=4"/></br>[SFoskitt](https://api.github.com/users/SFoskitt) | <img width="50" src="https://avatars2.githubusercontent.com/u/505011?v=4"/></br>[kcrt](https://api.github.com/users/kcrt) | <img width="50" src="https://avatars1.githubusercontent.com/u/538584?v=4"/></br>[xissy](https://api.github.com/users/xissy) | <img width="50" src="https://avatars3.githubusercontent.com/u/466122?v=4"/></br>[Tekki](https://api.github.com/users/Tekki) | <img width="50" src="https://avatars0.githubusercontent.com/u/21969426?v=4"/></br>[TheoDutch](https://api.github.com/users/TheoDutch) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/8731922?v=4"/></br>[tbroadley](https://api.github.com/users/tbroadley) | <img width="50" src="https://avatars1.githubusercontent.com/u/114300?v=4"/></br>[Kriechi](https://api.github.com/users/Kriechi) | <img width="50" src="https://avatars0.githubusercontent.com/u/3457339?v=4"/></br>[tkilaker](https://api.github.com/users/tkilaker) | <img width="50" src="https://avatars1.githubusercontent.com/u/4201229?v=4"/></br>[tcyrus](https://api.github.com/users/tcyrus) | <img width="50" src="https://avatars2.githubusercontent.com/u/834914?v=4"/></br>[tobias-grasse](https://api.github.com/users/tobias-grasse) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/6691273?v=4"/></br>[strobeltobias](https://api.github.com/users/strobeltobias) | <img width="50" src="https://avatars2.githubusercontent.com/u/70296?v=4"/></br>[tbergeron](https://api.github.com/users/tbergeron) | <img width="50" src="https://avatars1.githubusercontent.com/u/10265443?v=4"/></br>[Ullas-Aithal](https://api.github.com/users/Ullas-Aithal) | <img width="50" src="https://avatars2.githubusercontent.com/u/6104498?v=4"/></br>[MyTheValentinus](https://api.github.com/users/MyTheValentinus) | <img width="50" src="https://avatars3.githubusercontent.com/u/26511487?v=4"/></br>[WisdomCode](https://api.github.com/users/WisdomCode) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/1921957?v=4"/></br>[xsak](https://api.github.com/users/xsak) | <img width="50" src="https://avatars2.githubusercontent.com/u/11031696?v=4"/></br>[ymitsos](https://api.github.com/users/ymitsos) | <img width="50" src="https://avatars3.githubusercontent.com/u/29891001?v=4"/></br>[jyuvaraj03](https://api.github.com/users/jyuvaraj03) | <img width="50" src="https://avatars0.githubusercontent.com/u/15380913?v=4"/></br>[kowalskidev](https://api.github.com/users/kowalskidev) | <img width="50" src="https://avatars0.githubusercontent.com/u/63324960?v=4"/></br>[abolishallprivateproperty](https://api.github.com/users/abolishallprivateproperty) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/11336076?v=4"/></br>[aerotog](https://api.github.com/users/aerotog) | <img width="50" src="https://avatars2.githubusercontent.com/u/49116134?v=4"/></br>[anihm136](https://api.github.com/users/anihm136) | <img width="50" src="https://avatars2.githubusercontent.com/u/35600612?v=4"/></br>[boring10](https://api.github.com/users/boring10) | <img width="50" src="https://avatars0.githubusercontent.com/u/35413451?v=4"/></br>[chenlhlinux](https://api.github.com/users/chenlhlinux) | <img width="50" src="https://avatars3.githubusercontent.com/u/30935096?v=4"/></br>[cybertramp](https://api.github.com/users/cybertramp) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/9694906?v=4"/></br>[delta-emil](https://api.github.com/users/delta-emil) | <img width="50" src="https://avatars0.githubusercontent.com/u/926263?v=4"/></br>[doc75](https://api.github.com/users/doc75) | <img width="50" src="https://avatars2.githubusercontent.com/u/2903013?v=4"/></br>[ebayer](https://api.github.com/users/ebayer) | <img width="50" src="https://avatars3.githubusercontent.com/u/701050?v=4"/></br>[espinosa](https://api.github.com/users/espinosa) | <img width="50" src="https://avatars1.githubusercontent.com/u/18619090?v=4"/></br>[exponentactivity](https://api.github.com/users/exponentactivity) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/16708935?v=4"/></br>[exprez135](https://api.github.com/users/exprez135) | <img width="50" src="https://avatars1.githubusercontent.com/u/9768112?v=4"/></br>[fab4x](https://api.github.com/users/fab4x) | <img width="50" src="https://avatars0.githubusercontent.com/u/47755037?v=4"/></br>[fabianski7](https://api.github.com/users/fabianski7) | <img width="50" src="https://avatars0.githubusercontent.com/u/14201321?v=4"/></br>[rasperepodvipodvert](https://api.github.com/users/rasperepodvipodvert) | <img width="50" src="https://avatars1.githubusercontent.com/u/748808?v=4"/></br>[gasolin](https://api.github.com/users/gasolin) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/47191051?v=4"/></br>[githubaccount073](https://api.github.com/users/githubaccount073) | <img width="50" src="https://avatars1.githubusercontent.com/u/11388094?v=4"/></br>[hydrandt](https://api.github.com/users/hydrandt) | <img width="50" src="https://avatars0.githubusercontent.com/u/557540?v=4"/></br>[jabdoa2](https://api.github.com/users/jabdoa2) | <img width="50" src="https://avatars3.githubusercontent.com/u/53862536?v=4"/></br>[johanvanheusden](https://api.github.com/users/johanvanheusden) | <img width="50" src="https://avatars1.githubusercontent.com/u/54991735?v=4"/></br>[krzysiekwie](https://api.github.com/users/krzysiekwie) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/12849008?v=4"/></br>[lighthousebulb](https://api.github.com/users/lighthousebulb) | <img width="50" src="https://avatars0.githubusercontent.com/u/4140247?v=4"/></br>[luzpaz](https://api.github.com/users/luzpaz) | <img width="50" src="https://avatars2.githubusercontent.com/u/30428258?v=4"/></br>[nmiquan](https://api.github.com/users/nmiquan) | <img width="50" src="https://avatars0.githubusercontent.com/u/31123054?v=4"/></br>[nullpointer666](https://api.github.com/users/nullpointer666) | <img width="50" src="https://avatars2.githubusercontent.com/u/2979926?v=4"/></br>[oscaretu](https://api.github.com/users/oscaretu) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/36965591?v=4"/></br>[daehruoydeef](https://api.github.com/users/daehruoydeef) | <img width="50" src="https://avatars1.githubusercontent.com/u/42961947?v=4"/></br>[pensierocrea](https://api.github.com/users/pensierocrea) | <img width="50" src="https://avatars3.githubusercontent.com/u/10206967?v=4"/></br>[rhtenhove](https://api.github.com/users/rhtenhove) | <img width="50" src="https://avatars2.githubusercontent.com/u/16728217?v=4"/></br>[rikanotank1](https://api.github.com/users/rikanotank1) | <img width="50" src="https://avatars1.githubusercontent.com/u/51550769?v=4"/></br>[rnbastos](https://api.github.com/users/rnbastos) |
|
||||
| <img width="50" src="https://avatars3.githubusercontent.com/u/14062932?v=4"/></br>[simonsan](https://api.github.com/users/simonsan) | <img width="50" src="https://avatars2.githubusercontent.com/u/5004545?v=4"/></br>[stellarpower](https://api.github.com/users/stellarpower) | <img width="50" src="https://avatars1.githubusercontent.com/u/12995773?v=4"/></br>[sumomo-99](https://api.github.com/users/sumomo-99) | <img width="50" src="https://avatars0.githubusercontent.com/u/6908872?v=4"/></br>[taw00](https://api.github.com/users/taw00) | <img width="50" src="https://avatars0.githubusercontent.com/u/10956653?v=4"/></br>[tcassaert](https://api.github.com/users/tcassaert) |
|
||||
| <img width="50" src="https://avatars1.githubusercontent.com/u/46327531?v=4"/></br>[vicoutorama](https://api.github.com/users/vicoutorama) | <img width="50" src="https://avatars0.githubusercontent.com/u/2216902?v=4"/></br>[xcffl](https://api.github.com/users/xcffl) | <img width="50" src="https://avatars2.githubusercontent.com/u/37692927?v=4"/></br>[zaoyifan](https://api.github.com/users/zaoyifan) | <img width="50" src="https://avatars3.githubusercontent.com/u/55245068?v=4"/></br>[zen-quo](https://api.github.com/users/zen-quo) | <img width="50" src="https://avatars0.githubusercontent.com/u/25315?v=4"/></br>[xcession](https://api.github.com/users/xcession) |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/34542665?v=4"/></br>[paventyang](https://api.github.com/users/paventyang) | <img width="50" src="https://avatars1.githubusercontent.com/u/1308646?v=4"/></br>[zhangmx](https://api.github.com/users/zhangmx) | | | |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1285584?v=4"/></br>[laurent22](https://github.com/laurent22) | <img width="50" src="https://avatars.githubusercontent.com/u/223439?v=4"/></br>[tessus](https://github.com/tessus) | <img width="50" src="https://avatars.githubusercontent.com/u/2179547?v=4"/></br>[CalebJohn](https://github.com/CalebJohn) | <img width="50" src="https://avatars.githubusercontent.com/u/1732810?v=4"/></br>[mic704b](https://github.com/mic704b) | <img width="50" src="https://avatars.githubusercontent.com/u/995612?v=4"/></br>[roman-r-m](https://github.com/roman-r-m) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/29672555?v=4"/></br>[genneko](https://github.com/genneko) | <img width="50" src="https://avatars.githubusercontent.com/u/63491353?v=4"/></br>[j-krl](https://github.com/j-krl) | <img width="50" src="https://avatars.githubusercontent.com/u/4553672?v=4"/></br>[tanrax](https://github.com/tanrax) | <img width="50" src="https://avatars.githubusercontent.com/u/30305957?v=4"/></br>[naviji](https://github.com/naviji) | <img width="50" src="https://avatars.githubusercontent.com/u/3542031?v=4"/></br>[PackElend](https://github.com/PackElend) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8701534?v=4"/></br>[rtmkrlv](https://github.com/rtmkrlv) | <img width="50" src="https://avatars.githubusercontent.com/u/10997189?v=4"/></br>[fmrtn](https://github.com/fmrtn) | <img width="50" src="https://avatars.githubusercontent.com/u/4374338?v=4"/></br>[potatogim](https://github.com/potatogim) | <img width="50" src="https://avatars.githubusercontent.com/u/6979755?v=4"/></br>[devonzuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars.githubusercontent.com/u/26695184?v=4"/></br>[anjulalk](https://github.com/anjulalk) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16101778?v=4"/></br>[gabcoh](https://github.com/gabcoh) | <img width="50" src="https://avatars.githubusercontent.com/u/10927304?v=4"/></br>[matsest](https://github.com/matsest) | <img width="50" src="https://avatars.githubusercontent.com/u/6319051?v=4"/></br>[abonte](https://github.com/abonte) | <img width="50" src="https://avatars.githubusercontent.com/u/1685517?v=4"/></br>[Abijeet](https://github.com/Abijeet) | <img width="50" src="https://avatars.githubusercontent.com/u/27751740?v=4"/></br>[ishantgupta777](https://github.com/ishantgupta777) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/24863925?v=4"/></br>[JackGruber](https://github.com/JackGruber) | <img width="50" src="https://avatars.githubusercontent.com/u/2063957?v=4"/></br>[Ardakilic](https://github.com/Ardakilic) | <img width="50" src="https://avatars.githubusercontent.com/u/44024553?v=4"/></br>[rabeehrz](https://github.com/rabeehrz) | <img width="50" src="https://avatars.githubusercontent.com/u/35633575?v=4"/></br>[coderrsid](https://github.com/coderrsid) | <img width="50" src="https://avatars.githubusercontent.com/u/208212?v=4"/></br>[foxmask](https://github.com/foxmask) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6557454?v=4"/></br>[innocuo](https://github.com/innocuo) | <img width="50" src="https://avatars.githubusercontent.com/u/54268438?v=4"/></br>[Rahulm2310](https://github.com/Rahulm2310) | <img width="50" src="https://avatars.githubusercontent.com/u/1904967?v=4"/></br>[readingsnail](https://github.com/readingsnail) | <img width="50" src="https://avatars.githubusercontent.com/u/7415668?v=4"/></br>[mablin7](https://github.com/mablin7) | <img width="50" src="https://avatars.githubusercontent.com/u/3985557?v=4"/></br>[XarisA](https://github.com/XarisA) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/49979415?v=4"/></br>[jonath92](https://github.com/jonath92) | <img width="50" src="https://avatars.githubusercontent.com/u/4237724?v=4"/></br>[alexdevero](https://github.com/alexdevero) | <img width="50" src="https://avatars.githubusercontent.com/u/35904727?v=4"/></br>[Runo-saduwa](https://github.com/Runo-saduwa) | <img width="50" src="https://avatars.githubusercontent.com/u/5365582?v=4"/></br>[marcosvega91](https://github.com/marcosvega91) | <img width="50" src="https://avatars.githubusercontent.com/u/37639389?v=4"/></br>[petrz12](https://github.com/petrz12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/51550769?v=4"/></br>[rnbastos](https://github.com/rnbastos) | <img width="50" src="https://avatars.githubusercontent.com/u/32396?v=4"/></br>[ProgramFan](https://github.com/ProgramFan) | <img width="50" src="https://avatars.githubusercontent.com/u/4245227?v=4"/></br>[zblesk](https://github.com/zblesk) | <img width="50" src="https://avatars.githubusercontent.com/u/5730052?v=4"/></br>[vsimkus](https://github.com/vsimkus) | <img width="50" src="https://avatars.githubusercontent.com/u/3194829?v=4"/></br>[moltenform](https://github.com/moltenform) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/36989112?v=4"/></br>[nishantwrp](https://github.com/nishantwrp) | <img width="50" src="https://avatars.githubusercontent.com/u/5199995?v=4"/></br>[zuphilip](https://github.com/zuphilip) | <img width="50" src="https://avatars.githubusercontent.com/u/54576074?v=4"/></br>[Rishabh-malhotraa](https://github.com/Rishabh-malhotraa) | <img width="50" src="https://avatars.githubusercontent.com/u/559346?v=4"/></br>[metbril](https://github.com/metbril) | <img width="50" src="https://avatars.githubusercontent.com/u/47623588?v=4"/></br>[WhiredPlanck](https://github.com/WhiredPlanck) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43657314?v=4"/></br>[milotype](https://github.com/milotype) | <img width="50" src="https://avatars.githubusercontent.com/u/32196447?v=4"/></br>[yaozeye](https://github.com/yaozeye) | <img width="50" src="https://avatars.githubusercontent.com/u/12264626?v=4"/></br>[ylc395](https://github.com/ylc395) | <img width="50" src="https://avatars.githubusercontent.com/u/17768566?v=4"/></br>[RenatoXSR](https://github.com/RenatoXSR) | <img width="50" src="https://avatars.githubusercontent.com/u/54888685?v=4"/></br>[RedDocMD](https://github.com/RedDocMD) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/31567272?v=4"/></br>[q1011](https://github.com/q1011) | <img width="50" src="https://avatars.githubusercontent.com/u/12906090?v=4"/></br>[amitsin6h](https://github.com/amitsin6h) | <img width="50" src="https://avatars.githubusercontent.com/u/628474?v=4"/></br>[Atalanttore](https://github.com/Atalanttore) | <img width="50" src="https://avatars.githubusercontent.com/u/42747216?v=4"/></br>[Mannivu](https://github.com/Mannivu) | <img width="50" src="https://avatars.githubusercontent.com/u/23281486?v=4"/></br>[martonpaulo](https://github.com/martonpaulo) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/390889?v=4"/></br>[mmahmoudian](https://github.com/mmahmoudian) | <img width="50" src="https://avatars.githubusercontent.com/u/4497566?v=4"/></br>[rccavalcanti](https://github.com/rccavalcanti) | <img width="50" src="https://avatars.githubusercontent.com/u/1540054?v=4"/></br>[ShaneKilkelly](https://github.com/ShaneKilkelly) | <img width="50" src="https://avatars.githubusercontent.com/u/7091080?v=4"/></br>[sinkuu](https://github.com/sinkuu) | <img width="50" src="https://avatars.githubusercontent.com/u/6734573?v=4"/></br>[stweil](https://github.com/stweil) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/692072?v=4"/></br>[conyx](https://github.com/conyx) | <img width="50" src="https://avatars.githubusercontent.com/u/49116134?v=4"/></br>[anihm136](https://github.com/anihm136) | <img width="50" src="https://avatars.githubusercontent.com/u/937861?v=4"/></br>[archont00](https://github.com/archont00) | <img width="50" src="https://avatars.githubusercontent.com/u/32770029?v=4"/></br>[bradmcl](https://github.com/bradmcl) | <img width="50" src="https://avatars.githubusercontent.com/u/22592201?v=4"/></br>[tfinnberg](https://github.com/tfinnberg) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8716226?v=4"/></br>[amandamcg](https://github.com/amandamcg) | <img width="50" src="https://avatars.githubusercontent.com/u/3870964?v=4"/></br>[marcushill](https://github.com/marcushill) | <img width="50" src="https://avatars.githubusercontent.com/u/102242?v=4"/></br>[nathanleiby](https://github.com/nathanleiby) | <img width="50" src="https://avatars.githubusercontent.com/u/226708?v=4"/></br>[RaphaelKimmig](https://github.com/RaphaelKimmig) | <img width="50" src="https://avatars.githubusercontent.com/u/20461071?v=4"/></br>[Vaso3](https://github.com/Vaso3) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/36303913?v=4"/></br>[sensor-freak](https://github.com/sensor-freak) | <img width="50" src="https://avatars.githubusercontent.com/u/63918341?v=4"/></br>[lkiThakur](https://github.com/lkiThakur) | <img width="50" src="https://avatars.githubusercontent.com/u/28987176?v=4"/></br>[infinity052](https://github.com/infinity052) | <img width="50" src="https://avatars.githubusercontent.com/u/21161146?v=4"/></br>[BartBucknill](https://github.com/BartBucknill) | <img width="50" src="https://avatars.githubusercontent.com/u/2494769?v=4"/></br>[mrwulf](https://github.com/mrwulf) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/560571?v=4"/></br>[chrisb86](https://github.com/chrisb86) | <img width="50" src="https://avatars.githubusercontent.com/u/1686759?v=4"/></br>[chrmoritz](https://github.com/chrmoritz) | <img width="50" src="https://avatars.githubusercontent.com/u/58074586?v=4"/></br>[Daeraxa](https://github.com/Daeraxa) | <img width="50" src="https://avatars.githubusercontent.com/u/71190696?v=4"/></br>[Elaborendum](https://github.com/Elaborendum) | <img width="50" src="https://avatars.githubusercontent.com/u/5001259?v=4"/></br>[ethan42411](https://github.com/ethan42411) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2733783?v=4"/></br>[JOJ0](https://github.com/JOJ0) | <img width="50" src="https://avatars.githubusercontent.com/u/17108695?v=4"/></br>[jalajcodes](https://github.com/jalajcodes) | <img width="50" src="https://avatars.githubusercontent.com/u/238088?v=4"/></br>[jblunck](https://github.com/jblunck) | <img width="50" src="https://avatars.githubusercontent.com/u/3140223?v=4"/></br>[jdrobertso](https://github.com/jdrobertso) | <img width="50" src="https://avatars.githubusercontent.com/u/37297218?v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/339645?v=4"/></br>[jmontane](https://github.com/jmontane) | <img width="50" src="https://avatars.githubusercontent.com/u/69011?v=4"/></br>[johanhammar](https://github.com/johanhammar) | <img width="50" src="https://avatars.githubusercontent.com/u/4168339?v=4"/></br>[solariz](https://github.com/solariz) | <img width="50" src="https://avatars.githubusercontent.com/u/25288?v=4"/></br>[maicki](https://github.com/maicki) | <img width="50" src="https://avatars.githubusercontent.com/u/2136373?v=4"/></br>[mjjzf](https://github.com/mjjzf) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/27608187?v=4"/></br>[rt-oliveira](https://github.com/rt-oliveira) | <img width="50" src="https://avatars.githubusercontent.com/u/2486806?v=4"/></br>[sebastienjust](https://github.com/sebastienjust) | <img width="50" src="https://avatars.githubusercontent.com/u/28362310?v=4"/></br>[sealch](https://github.com/sealch) | <img width="50" src="https://avatars.githubusercontent.com/u/34258070?v=4"/></br>[StarFang208](https://github.com/StarFang208) | <img width="50" src="https://avatars.githubusercontent.com/u/59690052?v=4"/></br>[Subhra264](https://github.com/Subhra264) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1782292?v=4"/></br>[SubodhDahal](https://github.com/SubodhDahal) | <img width="50" src="https://avatars.githubusercontent.com/u/5912371?v=4"/></br>[TobiasDev](https://github.com/TobiasDev) | <img width="50" src="https://avatars.githubusercontent.com/u/13502069?v=4"/></br>[Whaell](https://github.com/Whaell) | <img width="50" src="https://avatars.githubusercontent.com/u/29891001?v=4"/></br>[jyuvaraj03](https://github.com/jyuvaraj03) | <img width="50" src="https://avatars.githubusercontent.com/u/15380913?v=4"/></br>[kowalskidev](https://github.com/kowalskidev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/337455?v=4"/></br>[alexchee](https://github.com/alexchee) | <img width="50" src="https://avatars.githubusercontent.com/u/5077221?v=4"/></br>[axq](https://github.com/axq) | <img width="50" src="https://avatars.githubusercontent.com/u/8808502?v=4"/></br>[barbowza](https://github.com/barbowza) | <img width="50" src="https://avatars.githubusercontent.com/u/42007357?v=4"/></br>[eresytter](https://github.com/eresytter) | <img width="50" src="https://avatars.githubusercontent.com/u/4316805?v=4"/></br>[lightray22](https://github.com/lightray22) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11711053?v=4"/></br>[lscolombo](https://github.com/lscolombo) | <img width="50" src="https://avatars.githubusercontent.com/u/36228623?v=4"/></br>[mrkaato](https://github.com/mrkaato) | <img width="50" src="https://avatars.githubusercontent.com/u/17399340?v=4"/></br>[pf-siedler](https://github.com/pf-siedler) | <img width="50" src="https://avatars.githubusercontent.com/u/17232523?v=4"/></br>[ruuti](https://github.com/ruuti) | <img width="50" src="https://avatars.githubusercontent.com/u/23638148?v=4"/></br>[s1nceri7y](https://github.com/s1nceri7y) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/10117386?v=4"/></br>[kornava](https://github.com/kornava) | <img width="50" src="https://avatars.githubusercontent.com/u/7471938?v=4"/></br>[ShuiHuo](https://github.com/ShuiHuo) | <img width="50" src="https://avatars.githubusercontent.com/u/11596277?v=4"/></br>[ikunya](https://github.com/ikunya) | <img width="50" src="https://avatars.githubusercontent.com/u/8184424?v=4"/></br>[Ahmad45123](https://github.com/Ahmad45123) | <img width="50" src="https://avatars.githubusercontent.com/u/59133880?v=4"/></br>[bedwardly-down](https://github.com/bedwardly-down) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/50335724?v=4"/></br>[dcaveiro](https://github.com/dcaveiro) | <img width="50" src="https://avatars.githubusercontent.com/u/47456195?v=4"/></br>[hexclover](https://github.com/hexclover) | <img width="50" src="https://avatars.githubusercontent.com/u/45535789?v=4"/></br>[2jaeyeol](https://github.com/2jaeyeol) | <img width="50" src="https://avatars.githubusercontent.com/u/25622825?v=4"/></br>[thackeraaron](https://github.com/thackeraaron) | <img width="50" src="https://avatars.githubusercontent.com/u/15862474?v=4"/></br>[aaronxn](https://github.com/aaronxn) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/40672207?v=4"/></br>[xUser5000](https://github.com/xUser5000) | <img width="50" src="https://avatars.githubusercontent.com/u/56785486?v=4"/></br>[iamabhi222](https://github.com/iamabhi222) | <img width="50" src="https://avatars.githubusercontent.com/u/63443657?v=4"/></br>[Aksh-Konda](https://github.com/Aksh-Konda) | <img width="50" src="https://avatars.githubusercontent.com/u/3660978?v=4"/></br>[alanfortlink](https://github.com/alanfortlink) | <img width="50" src="https://avatars.githubusercontent.com/u/53372753?v=4"/></br>[AverageUser2](https://github.com/AverageUser2) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4056990?v=4"/></br>[afischer211](https://github.com/afischer211) | <img width="50" src="https://avatars.githubusercontent.com/u/26230870?v=4"/></br>[a13xk](https://github.com/a13xk) | <img width="50" src="https://avatars.githubusercontent.com/u/14836659?v=4"/></br>[apankratov](https://github.com/apankratov) | <img width="50" src="https://avatars.githubusercontent.com/u/7045739?v=4"/></br>[teterkin](https://github.com/teterkin) | <img width="50" src="https://avatars.githubusercontent.com/u/215668?v=4"/></br>[avanderberg](https://github.com/avanderberg) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/41290751?v=4"/></br>[serenitatis](https://github.com/serenitatis) | <img width="50" src="https://avatars.githubusercontent.com/u/4408379?v=4"/></br>[lex111](https://github.com/lex111) | <img width="50" src="https://avatars.githubusercontent.com/u/60134194?v=4"/></br>[Alkindi42](https://github.com/Alkindi42) | <img width="50" src="https://avatars.githubusercontent.com/u/7129815?v=4"/></br>[Jumanjii](https://github.com/Jumanjii) | <img width="50" src="https://avatars.githubusercontent.com/u/19962243?v=4"/></br>[AlphaJack](https://github.com/AlphaJack) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/65647302?v=4"/></br>[Lord-Aman](https://github.com/Lord-Aman) | <img width="50" src="https://avatars.githubusercontent.com/u/14096959?v=4"/></br>[richtwin567](https://github.com/richtwin567) | <img width="50" src="https://avatars.githubusercontent.com/u/487182?v=4"/></br>[ajilderda](https://github.com/ajilderda) | <img width="50" src="https://avatars.githubusercontent.com/u/922429?v=4"/></br>[adrynov](https://github.com/adrynov) | <img width="50" src="https://avatars.githubusercontent.com/u/94937?v=4"/></br>[andrewperry](https://github.com/andrewperry) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5417051?v=4"/></br>[tekdel](https://github.com/tekdel) | <img width="50" src="https://avatars.githubusercontent.com/u/54475686?v=4"/></br>[anshuman9999](https://github.com/anshuman9999) | <img width="50" src="https://avatars.githubusercontent.com/u/25694659?v=4"/></br>[rasklaad](https://github.com/rasklaad) | <img width="50" src="https://avatars.githubusercontent.com/u/17809291?v=4"/></br>[Technik-J](https://github.com/Technik-J) | <img width="50" src="https://avatars.githubusercontent.com/u/498326?v=4"/></br>[Shaxine](https://github.com/Shaxine) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9095073?v=4"/></br>[antonio-ramadas](https://github.com/antonio-ramadas) | <img width="50" src="https://avatars.githubusercontent.com/u/28067395?v=4"/></br>[heyapoorva](https://github.com/heyapoorva) | <img width="50" src="https://avatars.githubusercontent.com/u/201215?v=4"/></br>[assimd](https://github.com/assimd) | <img width="50" src="https://avatars.githubusercontent.com/u/26827848?v=4"/></br>[Atrate](https://github.com/Atrate) | <img width="50" src="https://avatars.githubusercontent.com/u/60288895?v=4"/></br>[Beowulf2](https://github.com/Beowulf2) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/7034200?v=4"/></br>[bimlas](https://github.com/bimlas) | <img width="50" src="https://avatars.githubusercontent.com/u/47641641?v=4"/></br>[brenobaptista](https://github.com/brenobaptista) | <img width="50" src="https://avatars.githubusercontent.com/u/60824?v=4"/></br>[brttbndr](https://github.com/brttbndr) | <img width="50" src="https://avatars.githubusercontent.com/u/16287077?v=4"/></br>[carlbordum](https://github.com/carlbordum) | <img width="50" src="https://avatars.githubusercontent.com/u/20382?v=4"/></br>[carlosedp](https://github.com/carlosedp) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/105843?v=4"/></br>[chaifeng](https://github.com/chaifeng) | <img width="50" src="https://avatars.githubusercontent.com/u/549349?v=4"/></br>[charles-e](https://github.com/charles-e) | <img width="50" src="https://avatars.githubusercontent.com/u/19870089?v=4"/></br>[cyy5358](https://github.com/cyy5358) | <img width="50" src="https://avatars.githubusercontent.com/u/32337926?v=4"/></br>[Chillu1](https://github.com/Chillu1) | <img width="50" src="https://avatars.githubusercontent.com/u/2348463?v=4"/></br>[Techwolf12](https://github.com/Techwolf12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2282880?v=4"/></br>[cloudtrends](https://github.com/cloudtrends) | <img width="50" src="https://avatars.githubusercontent.com/u/17257053?v=4"/></br>[idcristi](https://github.com/idcristi) | <img width="50" src="https://avatars.githubusercontent.com/u/15956322?v=4"/></br>[damienmascre](https://github.com/damienmascre) | <img width="50" src="https://avatars.githubusercontent.com/u/1044056?v=4"/></br>[daniellandau](https://github.com/daniellandau) | <img width="50" src="https://avatars.githubusercontent.com/u/12847693?v=4"/></br>[danil-tolkachev](https://github.com/danil-tolkachev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/7279100?v=4"/></br>[darshani28](https://github.com/darshani28) | <img width="50" src="https://avatars.githubusercontent.com/u/26189247?v=4"/></br>[daukadolt](https://github.com/daukadolt) | <img width="50" src="https://avatars.githubusercontent.com/u/28535750?v=4"/></br>[NeverMendel](https://github.com/NeverMendel) | <img width="50" src="https://avatars.githubusercontent.com/u/26790323?v=4"/></br>[dervist](https://github.com/dervist) | <img width="50" src="https://avatars.githubusercontent.com/u/11378282?v=4"/></br>[diego-betto](https://github.com/diego-betto) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/215270?v=4"/></br>[erdody](https://github.com/erdody) | <img width="50" src="https://avatars.githubusercontent.com/u/10371667?v=4"/></br>[domgoodwin](https://github.com/domgoodwin) | <img width="50" src="https://avatars.githubusercontent.com/u/72066?v=4"/></br>[b4mboo](https://github.com/b4mboo) | <img width="50" src="https://avatars.githubusercontent.com/u/5131923?v=4"/></br>[donbowman](https://github.com/donbowman) | <img width="50" src="https://avatars.githubusercontent.com/u/579727?v=4"/></br>[sirnacnud](https://github.com/sirnacnud) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47756?v=4"/></br>[dflock](https://github.com/dflock) | <img width="50" src="https://avatars.githubusercontent.com/u/7990534?v=4"/></br>[drobilica](https://github.com/drobilica) | <img width="50" src="https://avatars.githubusercontent.com/u/21699905?v=4"/></br>[educbraga](https://github.com/educbraga) | <img width="50" src="https://avatars.githubusercontent.com/u/67867099?v=4"/></br>[eduardokimmel](https://github.com/eduardokimmel) | <img width="50" src="https://avatars.githubusercontent.com/u/30393516?v=4"/></br>[VodeniZeko](https://github.com/VodeniZeko) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/17415256?v=4"/></br>[ei-ke](https://github.com/ei-ke) | <img width="50" src="https://avatars.githubusercontent.com/u/1962738?v=4"/></br>[einverne](https://github.com/einverne) | <img width="50" src="https://avatars.githubusercontent.com/u/16492558?v=4"/></br>[eodeluga](https://github.com/eodeluga) | <img width="50" src="https://avatars.githubusercontent.com/u/16875937?v=4"/></br>[fathyar](https://github.com/fathyar) | <img width="50" src="https://avatars.githubusercontent.com/u/3057302?v=4"/></br>[fer22f](https://github.com/fer22f) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43272148?v=4"/></br>[fpindado](https://github.com/fpindado) | <img width="50" src="https://avatars.githubusercontent.com/u/1714374?v=4"/></br>[FleischKarussel](https://github.com/FleischKarussel) | <img width="50" src="https://avatars.githubusercontent.com/u/18525376?v=4"/></br>[talkdirty](https://github.com/talkdirty) | <img width="50" src="https://avatars.githubusercontent.com/u/19814827?v=4"/></br>[gmaubach](https://github.com/gmaubach) | <img width="50" src="https://avatars.githubusercontent.com/u/6190183?v=4"/></br>[gmag11](https://github.com/gmag11) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6209647?v=4"/></br>[Jackymancs4](https://github.com/Jackymancs4) | <img width="50" src="https://avatars.githubusercontent.com/u/297578?v=4"/></br>[Glandos](https://github.com/Glandos) | <img width="50" src="https://avatars.githubusercontent.com/u/24235344?v=4"/></br>[vibraniumdev](https://github.com/vibraniumdev) | <img width="50" src="https://avatars.githubusercontent.com/u/2257024?v=4"/></br>[gusbemacbe](https://github.com/gusbemacbe) | <img width="50" src="https://avatars.githubusercontent.com/u/64917442?v=4"/></br>[HOLLYwyh](https://github.com/HOLLYwyh) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/18524580?v=4"/></br>[Fvbor](https://github.com/Fvbor) | <img width="50" src="https://avatars.githubusercontent.com/u/22606250?v=4"/></br>[bennetthanna](https://github.com/bennetthanna) | <img width="50" src="https://avatars.githubusercontent.com/u/67231570?v=4"/></br>[harshitkathuria](https://github.com/harshitkathuria) | <img width="50" src="https://avatars.githubusercontent.com/u/1716229?v=4"/></br>[Vistaus](https://github.com/Vistaus) | <img width="50" src="https://avatars.githubusercontent.com/u/6509881?v=4"/></br>[ianjs](https://github.com/ianjs) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/19862172?v=4"/></br>[iahmedbacha](https://github.com/iahmedbacha) | <img width="50" src="https://avatars.githubusercontent.com/u/1533624?v=4"/></br>[IrvinDominin](https://github.com/IrvinDominin) | <img width="50" src="https://avatars.githubusercontent.com/u/33200024?v=4"/></br>[ishammahajan](https://github.com/ishammahajan) | <img width="50" src="https://avatars.githubusercontent.com/u/6916297?v=4"/></br>[ffadilaputra](https://github.com/ffadilaputra) | <img width="50" src="https://avatars.githubusercontent.com/u/19985741?v=4"/></br>[JRaiden16](https://github.com/JRaiden16) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11466782?v=4"/></br>[jacobherrington](https://github.com/jacobherrington) | <img width="50" src="https://avatars.githubusercontent.com/u/9365179?v=4"/></br>[jamesadjinwa](https://github.com/jamesadjinwa) | <img width="50" src="https://avatars.githubusercontent.com/u/20801821?v=4"/></br>[jrwrigh](https://github.com/jrwrigh) | <img width="50" src="https://avatars.githubusercontent.com/u/4995433?v=4"/></br>[jaredcrowe](https://github.com/jaredcrowe) | <img width="50" src="https://avatars.githubusercontent.com/u/4087105?v=4"/></br>[volatilevar](https://github.com/volatilevar) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47724360?v=4"/></br>[innkuika](https://github.com/innkuika) | <img width="50" src="https://avatars.githubusercontent.com/u/163555?v=4"/></br>[JoelRSimpson](https://github.com/JoelRSimpson) | <img width="50" src="https://avatars.githubusercontent.com/u/6965062?v=4"/></br>[joeltaylor](https://github.com/joeltaylor) | <img width="50" src="https://avatars.githubusercontent.com/u/242107?v=4"/></br>[exic](https://github.com/exic) | <img width="50" src="https://avatars.githubusercontent.com/u/13716151?v=4"/></br>[JonathanPlasse](https://github.com/JonathanPlasse) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1248504?v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars.githubusercontent.com/u/6048003?v=4"/></br>[joybinchen](https://github.com/joybinchen) | <img width="50" src="https://avatars.githubusercontent.com/u/37601331?v=4"/></br>[kaustubhsh](https://github.com/kaustubhsh) | <img width="50" src="https://avatars.githubusercontent.com/u/1560189?v=4"/></br>[y-usuzumi](https://github.com/y-usuzumi) | <img width="50" src="https://avatars.githubusercontent.com/u/1660460?v=4"/></br>[xuhcc](https://github.com/xuhcc) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16933735?v=4"/></br>[kirtanprht](https://github.com/kirtanprht) | <img width="50" src="https://avatars.githubusercontent.com/u/37491732?v=4"/></br>[k0ur0x](https://github.com/k0ur0x) | <img width="50" src="https://avatars.githubusercontent.com/u/7824233?v=4"/></br>[kklas](https://github.com/kklas) | <img width="50" src="https://avatars.githubusercontent.com/u/8622992?v=4"/></br>[xmlangel](https://github.com/xmlangel) | <img width="50" src="https://avatars.githubusercontent.com/u/1055100?v=4"/></br>[troilus](https://github.com/troilus) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2599210?v=4"/></br>[lboullo0](https://github.com/lboullo0) | <img width="50" src="https://avatars.githubusercontent.com/u/1562062?v=4"/></br>[dbinary](https://github.com/dbinary) | <img width="50" src="https://avatars.githubusercontent.com/u/15436007?v=4"/></br>[marc-bouvier](https://github.com/marc-bouvier) | <img width="50" src="https://avatars.githubusercontent.com/u/5699725?v=4"/></br>[mvonmaltitz](https://github.com/mvonmaltitz) | <img width="50" src="https://avatars.githubusercontent.com/u/11036464?v=4"/></br>[mlkood](https://github.com/mlkood) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2480960?v=4"/></br>[plextoriano](https://github.com/plextoriano) | <img width="50" src="https://avatars.githubusercontent.com/u/5788516?v=4"/></br>[Marmo](https://github.com/Marmo) | <img width="50" src="https://avatars.githubusercontent.com/u/29300939?v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars.githubusercontent.com/u/640949?v=4"/></br>[freaktechnik](https://github.com/freaktechnik) | <img width="50" src="https://avatars.githubusercontent.com/u/79802125?v=4"/></br>[martinkorelic](https://github.com/martinkorelic) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/287105?v=4"/></br>[Petemir](https://github.com/Petemir) | <img width="50" src="https://avatars.githubusercontent.com/u/5218859?v=4"/></br>[matsair](https://github.com/matsair) | <img width="50" src="https://avatars.githubusercontent.com/u/12831489?v=4"/></br>[mgroth0](https://github.com/mgroth0) | <img width="50" src="https://avatars.githubusercontent.com/u/21796?v=4"/></br>[silentmatt](https://github.com/silentmatt) | <img width="50" src="https://avatars.githubusercontent.com/u/76700192?v=4"/></br>[maxs-test](https://github.com/maxs-test) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/59669349?v=4"/></br>[MichBoi](https://github.com/MichBoi) | <img width="50" src="https://avatars.githubusercontent.com/u/51273874?v=4"/></br>[MichipX](https://github.com/MichipX) | <img width="50" src="https://avatars.githubusercontent.com/u/53177864?v=4"/></br>[MrTraduttore](https://github.com/MrTraduttore) | <img width="50" src="https://avatars.githubusercontent.com/u/48156230?v=4"/></br>[sanjarcode](https://github.com/sanjarcode) | <img width="50" src="https://avatars.githubusercontent.com/u/43955099?v=4"/></br>[Mustafa-ALD](https://github.com/Mustafa-ALD) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9076687?v=4"/></br>[NJannasch](https://github.com/NJannasch) | <img width="50" src="https://avatars.githubusercontent.com/u/8016073?v=4"/></br>[zomglings](https://github.com/zomglings) | <img width="50" src="https://avatars.githubusercontent.com/u/10386884?v=4"/></br>[Frichetten](https://github.com/Frichetten) | <img width="50" src="https://avatars.githubusercontent.com/u/5541611?v=4"/></br>[nicolas-suzuki](https://github.com/nicolas-suzuki) | <img width="50" src="https://avatars.githubusercontent.com/u/12369770?v=4"/></br>[Ouvill](https://github.com/Ouvill) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43815417?v=4"/></br>[shorty2380](https://github.com/shorty2380) | <img width="50" src="https://avatars.githubusercontent.com/u/15014287?v=4"/></br>[dist3r](https://github.com/dist3r) | <img width="50" src="https://avatars.githubusercontent.com/u/19418601?v=4"/></br>[rakleed](https://github.com/rakleed) | <img width="50" src="https://avatars.githubusercontent.com/u/7881932?v=4"/></br>[idle-code](https://github.com/idle-code) | <img width="50" src="https://avatars.githubusercontent.com/u/168931?v=4"/></br>[bobchao](https://github.com/bobchao) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6306608?v=4"/></br>[Diadlo](https://github.com/Diadlo) | <img width="50" src="https://avatars.githubusercontent.com/u/42793024?v=4"/></br>[pranavmodx](https://github.com/pranavmodx) | <img width="50" src="https://avatars.githubusercontent.com/u/50834839?v=4"/></br>[R3dError](https://github.com/R3dError) | <img width="50" src="https://avatars.githubusercontent.com/u/42652941?v=4"/></br>[rajprakash00](https://github.com/rajprakash00) | <img width="50" src="https://avatars.githubusercontent.com/u/32304956?v=4"/></br>[rahil1304](https://github.com/rahil1304) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8257474?v=4"/></br>[rasulkireev](https://github.com/rasulkireev) | <img width="50" src="https://avatars.githubusercontent.com/u/17312341?v=4"/></br>[reinhart1010](https://github.com/reinhart1010) | <img width="50" src="https://avatars.githubusercontent.com/u/60484714?v=4"/></br>[Retew](https://github.com/Retew) | <img width="50" src="https://avatars.githubusercontent.com/u/10456131?v=4"/></br>[ambrt](https://github.com/ambrt) | <img width="50" src="https://avatars.githubusercontent.com/u/15892014?v=4"/></br>[Derkades](https://github.com/Derkades) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/49439044?v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars.githubusercontent.com/u/54365?v=4"/></br>[rodgco](https://github.com/rodgco) | <img width="50" src="https://avatars.githubusercontent.com/u/96014?v=4"/></br>[Ronnie76er](https://github.com/Ronnie76er) | <img width="50" src="https://avatars.githubusercontent.com/u/79168?v=4"/></br>[roryokane](https://github.com/roryokane) | <img width="50" src="https://avatars.githubusercontent.com/u/744655?v=4"/></br>[ruzaq](https://github.com/ruzaq) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/20490839?v=4"/></br>[szokesandor](https://github.com/szokesandor) | <img width="50" src="https://avatars.githubusercontent.com/u/19328605?v=4"/></br>[SamuelBlickle](https://github.com/SamuelBlickle) | <img width="50" src="https://avatars.githubusercontent.com/u/80849457?v=4"/></br>[livingc0l0ur](https://github.com/livingc0l0ur) | <img width="50" src="https://avatars.githubusercontent.com/u/1776?v=4"/></br>[bronson](https://github.com/bronson) | <img width="50" src="https://avatars.githubusercontent.com/u/24606935?v=4"/></br>[semperor](https://github.com/semperor) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/607938?v=4"/></br>[shawnaxsom](https://github.com/shawnaxsom) | <img width="50" src="https://avatars.githubusercontent.com/u/9937486?v=4"/></br>[SFoskitt](https://github.com/SFoskitt) | <img width="50" src="https://avatars.githubusercontent.com/u/505011?v=4"/></br>[kcrt](https://github.com/kcrt) | <img width="50" src="https://avatars.githubusercontent.com/u/538584?v=4"/></br>[xissy](https://github.com/xissy) | <img width="50" src="https://avatars.githubusercontent.com/u/164962?v=4"/></br>[tams](https://github.com/tams) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/466122?v=4"/></br>[Tekki](https://github.com/Tekki) | <img width="50" src="https://avatars.githubusercontent.com/u/2112477?v=4"/></br>[ThatcherC](https://github.com/ThatcherC) | <img width="50" src="https://avatars.githubusercontent.com/u/21969426?v=4"/></br>[TheoDutch](https://github.com/TheoDutch) | <img width="50" src="https://avatars.githubusercontent.com/u/8731922?v=4"/></br>[tbroadley](https://github.com/tbroadley) | <img width="50" src="https://avatars.githubusercontent.com/u/114300?v=4"/></br>[Kriechi](https://github.com/Kriechi) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/3457339?v=4"/></br>[tkilaker](https://github.com/tkilaker) | <img width="50" src="https://avatars.githubusercontent.com/u/802148?v=4"/></br>[Tim-Erwin](https://github.com/Tim-Erwin) | <img width="50" src="https://avatars.githubusercontent.com/u/4201229?v=4"/></br>[tcyrus](https://github.com/tcyrus) | <img width="50" src="https://avatars.githubusercontent.com/u/834914?v=4"/></br>[tobias-grasse](https://github.com/tobias-grasse) | <img width="50" src="https://avatars.githubusercontent.com/u/6691273?v=4"/></br>[strobeltobias](https://github.com/strobeltobias) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1677578?v=4"/></br>[kostegit](https://github.com/kostegit) | <img width="50" src="https://avatars.githubusercontent.com/u/70296?v=4"/></br>[tbergeron](https://github.com/tbergeron) | <img width="50" src="https://avatars.githubusercontent.com/u/10265443?v=4"/></br>[Ullas-Aithal](https://github.com/Ullas-Aithal) | <img width="50" src="https://avatars.githubusercontent.com/u/6104498?v=4"/></br>[MyTheValentinus](https://github.com/MyTheValentinus) | <img width="50" src="https://avatars.githubusercontent.com/u/2830093?v=4"/></br>[vassudanagunta](https://github.com/vassudanagunta) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/54314949?v=4"/></br>[vijayjoshi16](https://github.com/vijayjoshi16) | <img width="50" src="https://avatars.githubusercontent.com/u/59287619?v=4"/></br>[max-keviv](https://github.com/max-keviv) | <img width="50" src="https://avatars.githubusercontent.com/u/598576?v=4"/></br>[vandreykiv](https://github.com/vandreykiv) | <img width="50" src="https://avatars.githubusercontent.com/u/26511487?v=4"/></br>[WisdomCode](https://github.com/WisdomCode) | <img width="50" src="https://avatars.githubusercontent.com/u/1921957?v=4"/></br>[xsak](https://github.com/xsak) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11031696?v=4"/></br>[ymitsos](https://github.com/ymitsos) | <img width="50" src="https://avatars.githubusercontent.com/u/63324960?v=4"/></br>[abolishallprivateproperty](https://github.com/abolishallprivateproperty) | <img width="50" src="https://avatars.githubusercontent.com/u/11336076?v=4"/></br>[aerotog](https://github.com/aerotog) | <img width="50" src="https://avatars.githubusercontent.com/u/39854348?v=4"/></br>[albertopasqualetto](https://github.com/albertopasqualetto) | <img width="50" src="https://avatars.githubusercontent.com/u/44570278?v=4"/></br>[asrient](https://github.com/asrient) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/621360?v=4"/></br>[bestlibre](https://github.com/bestlibre) | <img width="50" src="https://avatars.githubusercontent.com/u/35600612?v=4"/></br>[boring10](https://github.com/boring10) | <img width="50" src="https://avatars.githubusercontent.com/u/13894820?v=4"/></br>[cadolphs](https://github.com/cadolphs) | <img width="50" src="https://avatars.githubusercontent.com/u/12461043?v=4"/></br>[colorchestra](https://github.com/colorchestra) | <img width="50" src="https://avatars.githubusercontent.com/u/30935096?v=4"/></br>[cybertramp](https://github.com/cybertramp) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/15824892?v=4"/></br>[dartero](https://github.com/dartero) | <img width="50" src="https://avatars.githubusercontent.com/u/9694906?v=4"/></br>[delta-emil](https://github.com/delta-emil) | <img width="50" src="https://avatars.githubusercontent.com/u/926263?v=4"/></br>[doc75](https://github.com/doc75) | <img width="50" src="https://avatars.githubusercontent.com/u/5589253?v=4"/></br>[dsp77](https://github.com/dsp77) | <img width="50" src="https://avatars.githubusercontent.com/u/2903013?v=4"/></br>[ebayer](https://github.com/ebayer) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9206310?v=4"/></br>[elsiehupp](https://github.com/elsiehupp) | <img width="50" src="https://avatars.githubusercontent.com/u/701050?v=4"/></br>[espinosa](https://github.com/espinosa) | <img width="50" src="https://avatars.githubusercontent.com/u/18619090?v=4"/></br>[exponentactivity](https://github.com/exponentactivity) | <img width="50" src="https://avatars.githubusercontent.com/u/16708935?v=4"/></br>[exprez135](https://github.com/exprez135) | <img width="50" src="https://avatars.githubusercontent.com/u/9768112?v=4"/></br>[fab4x](https://github.com/fab4x) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47755037?v=4"/></br>[fabianski7](https://github.com/fabianski7) | <img width="50" src="https://avatars.githubusercontent.com/u/14201321?v=4"/></br>[rasperepodvipodvert](https://github.com/rasperepodvipodvert) | <img width="50" src="https://avatars.githubusercontent.com/u/748808?v=4"/></br>[gasolin](https://github.com/gasolin) | <img width="50" src="https://avatars.githubusercontent.com/u/47191051?v=4"/></br>[githubaccount073](https://github.com/githubaccount073) | <img width="50" src="https://avatars.githubusercontent.com/u/43672033?v=4"/></br>[hms5232](https://github.com/hms5232) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11388094?v=4"/></br>[hydrandt](https://github.com/hydrandt) | <img width="50" src="https://avatars.githubusercontent.com/u/61012185?v=4"/></br>[iamtalwinder](https://github.com/iamtalwinder) | <img width="50" src="https://avatars.githubusercontent.com/u/557540?v=4"/></br>[jabdoa2](https://github.com/jabdoa2) | <img width="50" src="https://avatars.githubusercontent.com/u/29166402?v=4"/></br>[jduar](https://github.com/jduar) | <img width="50" src="https://avatars.githubusercontent.com/u/2678545?v=4"/></br>[jibedoubleve](https://github.com/jibedoubleve) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/53862536?v=4"/></br>[johanvanheusden](https://github.com/johanvanheusden) | <img width="50" src="https://avatars.githubusercontent.com/u/38327267?v=4"/></br>[jtagcat](https://github.com/jtagcat) | <img width="50" src="https://avatars.githubusercontent.com/u/61631665?v=4"/></br>[konhi](https://github.com/konhi) | <img width="50" src="https://avatars.githubusercontent.com/u/54991735?v=4"/></br>[krzysiekwie](https://github.com/krzysiekwie) | <img width="50" src="https://avatars.githubusercontent.com/u/12849008?v=4"/></br>[lighthousebulb](https://github.com/lighthousebulb) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4140247?v=4"/></br>[luzpaz](https://github.com/luzpaz) | <img width="50" src="https://avatars.githubusercontent.com/u/29355048?v=4"/></br>[majsterkovic](https://github.com/majsterkovic) | <img width="50" src="https://avatars.githubusercontent.com/u/77744862?v=4"/></br>[mak2002](https://github.com/mak2002) | <img width="50" src="https://avatars.githubusercontent.com/u/30428258?v=4"/></br>[nmiquan](https://github.com/nmiquan) | <img width="50" src="https://avatars.githubusercontent.com/u/31123054?v=4"/></br>[nullpointer666](https://github.com/nullpointer666) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2979926?v=4"/></br>[oscaretu](https://github.com/oscaretu) | <img width="50" src="https://avatars.githubusercontent.com/u/36965591?v=4"/></br>[oskarsh](https://github.com/oskarsh) | <img width="50" src="https://avatars.githubusercontent.com/u/52031346?v=4"/></br>[osso73](https://github.com/osso73) | <img width="50" src="https://avatars.githubusercontent.com/u/29743024?v=4"/></br>[over-soul](https://github.com/over-soul) | <img width="50" src="https://avatars.githubusercontent.com/u/42961947?v=4"/></br>[pensierocrea](https://github.com/pensierocrea) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/45542782?v=4"/></br>[pomeloy](https://github.com/pomeloy) | <img width="50" src="https://avatars.githubusercontent.com/u/10206967?v=4"/></br>[rhtenhove](https://github.com/rhtenhove) | <img width="50" src="https://avatars.githubusercontent.com/u/16728217?v=4"/></br>[rikanotank1](https://github.com/rikanotank1) | <img width="50" src="https://avatars.githubusercontent.com/u/24560368?v=4"/></br>[rxliuli](https://github.com/rxliuli) | <img width="50" src="https://avatars.githubusercontent.com/u/14062932?v=4"/></br>[simonsan](https://github.com/simonsan) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5004545?v=4"/></br>[stellarpower](https://github.com/stellarpower) | <img width="50" src="https://avatars.githubusercontent.com/u/20983267?v=4"/></br>[suixinio](https://github.com/suixinio) | <img width="50" src="https://avatars.githubusercontent.com/u/12995773?v=4"/></br>[sumomo-99](https://github.com/sumomo-99) | <img width="50" src="https://avatars.githubusercontent.com/u/367170?v=4"/></br>[xtatsux](https://github.com/xtatsux) | <img width="50" src="https://avatars.githubusercontent.com/u/6908872?v=4"/></br>[taw00](https://github.com/taw00) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/10956653?v=4"/></br>[tcassaert](https://github.com/tcassaert) | <img width="50" src="https://avatars.githubusercontent.com/u/46327531?v=4"/></br>[victante](https://github.com/victante) | <img width="50" src="https://avatars.githubusercontent.com/u/7252567?v=4"/></br>[Voltinus](https://github.com/Voltinus) | <img width="50" src="https://avatars.githubusercontent.com/u/2216902?v=4"/></br>[xcffl](https://github.com/xcffl) | <img width="50" src="https://avatars.githubusercontent.com/u/46404814?v=4"/></br>[yourcontact](https://github.com/yourcontact) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/37692927?v=4"/></br>[zaoyifan](https://github.com/zaoyifan) | <img width="50" src="https://avatars.githubusercontent.com/u/10813608?v=4"/></br>[zawnk](https://github.com/zawnk) | <img width="50" src="https://avatars.githubusercontent.com/u/55245068?v=4"/></br>[zen-quo](https://github.com/zen-quo) | <img width="50" src="https://avatars.githubusercontent.com/u/23507174?v=4"/></br>[zozolina123](https://github.com/zozolina123) | <img width="50" src="https://avatars.githubusercontent.com/u/25315?v=4"/></br>[xcession](https://github.com/xcession) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/34542665?v=4"/></br>[paventyang](https://github.com/paventyang) | <img width="50" src="https://avatars.githubusercontent.com/u/608014?v=4"/></br>[jackytsu](https://github.com/jackytsu) | <img width="50" src="https://avatars.githubusercontent.com/u/1308646?v=4"/></br>[zhangmx](https://github.com/zhangmx) | | |
|
||||
<!-- CONTRIBUTORS-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Known bugs
|
||||
|
@@ -9,6 +9,8 @@ version: '3'
|
||||
services:
|
||||
db:
|
||||
image: postgres:13.1
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
|
@@ -108,11 +108,12 @@
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Defines whether the command should be enabled or disabled, which in turns affects
|
||||
the enabled state of any associated button or menu item.</p>
|
||||
<p>Defines whether the command should be enabled or disabled, which in turns
|
||||
affects the enabled state of any associated button or menu item.</p>
|
||||
</div>
|
||||
<p>The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
|
||||
<code>true</code> or <code>false</code>. It supports the following operators:</p>
|
||||
<p>The condition should be expressed as a "when-clause" (as in Visual Studio
|
||||
Code). It's a simple boolean expression that evaluates to <code>true</code> or
|
||||
<code>false</code>. It supports the following operators:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -142,7 +143,17 @@
|
||||
<td>"oneNoteSelected && !inConflictFolder"</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>Currently the supported context variables aren't documented, but you can <a href="https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts">find the list here</a>.</p>
|
||||
<p>Joplin, unlike VSCode, also supports parenthesis, which allows creating
|
||||
more complex expressions such as <code>cond1 || (cond2 && cond3)</code>. Only one
|
||||
level of parenthesis is possible (nested ones aren't supported).</p>
|
||||
<p>Currently the supported context variables aren't documented, but you can
|
||||
find the list below:</p>
|
||||
<ul>
|
||||
<li>[Global When
|
||||
Clauses](<a href="https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts">https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts</a>).</li>
|
||||
<li>[Desktop app When
|
||||
Clauses](<a href="https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts">https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts</a>).</li>
|
||||
</ul>
|
||||
<p>Note: Commands are enabled by default unless you use this property.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -721,6 +721,11 @@ async function fetchAllNotes() {
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>share_id</td>
|
||||
<td>text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>body_html</td>
|
||||
<td>text</td>
|
||||
<td>Note body, in HTML format</td>
|
||||
@@ -841,6 +846,11 @@ async function fetchAllNotes() {
|
||||
<td>int</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>share_id</td>
|
||||
<td>text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>GET /folders<a name="get-folders" href="#get-folders" class="heading-anchor">🔗</a></h2>
|
||||
@@ -937,6 +947,11 @@ async function fetchAllNotes() {
|
||||
<td>int</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>share_id</td>
|
||||
<td>text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>GET /resources<a name="get-resources" href="#get-resources" class="heading-anchor">🔗</a></h2>
|
||||
|
@@ -405,6 +405,46 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog.md
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate using PayPal"></a> <a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a> <a href="https://joplinapp.org/donate/#donations"><img src="https://joplinapp.org/images/badges/Donate-IBAN.svg" alt="Donate using IBAN"></a></p>
|
||||
<hr>
|
||||
<h1>Joplin changelog<a name="joplin-changelog" href="#joplin-changelog" class="heading-anchor">🔗</a></h1>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v2.0.2">v2.0.2</a> (Pre-release) - 2021-05-21T18:07:48Z<a name="v2-0-2-https-github-com-laurent22-joplin-releases-tag-v2-0-2-pre-release-2021-05-21t18-07-48z" href="#v2-0-2-https-github-com-laurent22-joplin-releases-tag-v2-0-2-pre-release-2021-05-21t18-07-48z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>New: Add Share Notebook menu item (6f2f241)</li>
|
||||
<li>New: Add classnames to DOM elements for theming purposes (<a href="https://github.com/laurent22/joplin/issues/4933">#4933</a> by <a href="https://github.com/ajilderda">@ajilderda</a>)</li>
|
||||
<li>Improved: Allow unsharing a note (f7d164b)</li>
|
||||
<li>Improved: Displays error info when Joplin Server fails (3f0586e)</li>
|
||||
<li>Improved: Handle too large items for Joplin Server (d29624c)</li>
|
||||
<li>Improved: Import SVG as images when importing ENEX files (<a href="https://github.com/laurent22/joplin/issues/4968">#4968</a>)</li>
|
||||
<li>Improved: Import linked local files when importing Markdown files (<a href="https://github.com/laurent22/joplin/issues/4966">#4966</a>) (<a href="https://github.com/laurent22/joplin/issues/4433">#4433</a> by <a href="https://github.com/JackGruber">@JackGruber</a>)</li>
|
||||
<li>Improved: Improved usability when plugin repository cannot be connected to (<a href="https://github.com/laurent22/joplin/issues/4462">#4462</a>)</li>
|
||||
<li>Improved: Made sync more reliable by making it skip items that time out, and improved sync status screen (15fe119)</li>
|
||||
<li>Improved: Pass custom CSS property to all export handlers and renderers (bd08041)</li>
|
||||
<li>Improved: Regression: It was no longer possible to add list items in an empty note (6577f4f)</li>
|
||||
<li>Improved: Regression: Pasting plain text in Rich Text editor was broken (9e9bf63)</li>
|
||||
<li>Fixed: Fixed issue with empty panels being created by plugins (<a href="https://github.com/laurent22/joplin/issues/4926">#4926</a>)</li>
|
||||
<li>Fixed: Fixed pasting HTML in Rich Text editor, and improved pasting plain text (2226b79)</li>
|
||||
<li>Fixed: Improved importing Evernote notes that contain codeblocks (<a href="https://github.com/laurent22/joplin/issues/4965">#4965</a>)</li>
|
||||
<li>Fixed: Prevent cursor from jumping to top of page when pasting image (<a href="https://github.com/laurent22/joplin/issues/4591">#4591</a>)</li>
|
||||
</ul>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v2.0.1">v2.0.1</a> (Pre-release) - 2021-05-15T13:22:58Z<a name="v2-0-1-https-github-com-laurent22-joplin-releases-tag-v2-0-1-pre-release-2021-05-15t13-22-58z" href="#v2-0-1-https-github-com-laurent22-joplin-releases-tag-v2-0-1-pre-release-2021-05-15t13-22-58z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>New: Add support for sharing notebooks with Joplin Server (<a href="https://github.com/laurent22/joplin/issues/4772">#4772</a>)</li>
|
||||
<li>New: Add new date format YYMMDD (<a href="https://github.com/laurent22/joplin/issues/4954">#4954</a> by Helmut K. C. Tessarek)</li>
|
||||
<li>New: Added button to skip an application update (a31b402)</li>
|
||||
<li>Fixed: Display proper error message when JEX file is corrupted (<a href="https://github.com/laurent22/joplin/issues/4958">#4958</a>)</li>
|
||||
<li>Fixed: Show or hide completed todos in search results based on user settings (<a href="https://github.com/laurent22/joplin/issues/4951">#4951</a>) (<a href="https://github.com/laurent22/joplin/issues/4581">#4581</a> by <a href="https://github.com/JackGruber">@JackGruber</a>)</li>
|
||||
<li>Fixed: Solve "Resource Id not provided" error (<a href="https://github.com/laurent22/joplin/issues/4943">#4943</a>) (<a href="https://github.com/laurent22/joplin/issues/4891">#4891</a> by <a href="https://github.com/Subhra264">@Subhra264</a>)</li>
|
||||
</ul>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.8.5">v1.8.5</a> - 2021-05-10T11:58:14Z<a name="v1-8-5-https-github-com-laurent22-joplin-releases-tag-v1-8-5-2021-05-10t11-58-14z" href="#v1-8-5-https-github-com-laurent22-joplin-releases-tag-v1-8-5-2021-05-10t11-58-14z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>Fixed: Fixed pasting of text and images from Word on Windows (<a href="https://github.com/laurent22/joplin/issues/4916">#4916</a>)</li>
|
||||
<li>Security: Filter out NOSCRIPT tags that could be used to cause an XSS (found by <a href="https://twitter.com/jubairfolder">Jubair Rehman Yousafzai</a>) (9c20d59)</li>
|
||||
</ul>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.8.4">v1.8.4</a> (Pre-release) - 2021-05-09T18:05:05Z<a name="v1-8-4-https-github-com-laurent22-joplin-releases-tag-v1-8-4-pre-release-2021-05-09t18-05-05z" href="#v1-8-4-https-github-com-laurent22-joplin-releases-tag-v1-8-4-pre-release-2021-05-09t18-05-05z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>Improved: Improve display of release notes for new versions (f76f99b)</li>
|
||||
<li>Fixed: Ensure that image paths that contain spaces are pasted correctly in the Rich Text editor (<a href="https://github.com/laurent22/joplin/issues/4916">#4916</a>)</li>
|
||||
<li>Fixed: Make sure sync startup operations are cleared after startup (<a href="https://github.com/laurent22/joplin/issues/4919">#4919</a>)</li>
|
||||
<li>Security: Apply npm audit security fixes (0b67446)</li>
|
||||
</ul>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.8.3">v1.8.3</a> (Pre-release) - 2021-05-04T10:38:16Z<a name="v1-8-3-https-github-com-laurent22-joplin-releases-tag-v1-8-3-pre-release-2021-05-04t10-38-16z" href="#v1-8-3-https-github-com-laurent22-joplin-releases-tag-v1-8-3-pre-release-2021-05-04t10-38-16z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>New: Add "id" and "due" search filters (<a href="https://github.com/laurent22/joplin/issues/4898">#4898</a> by <a href="https://github.com/JackGruber">@JackGruber</a>)</li>
|
||||
|
443
docs/changelog_android/index.html
Normal file
443
docs/changelog_android/index.html
Normal file
@@ -0,0 +1,443 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<!--
|
||||
|
||||
!!! WARNING !!!
|
||||
|
||||
This file was auto-generated from readme/changelog_android.md and any manual change
|
||||
made to it will be overwritten. To make a change to this file please modify
|
||||
the source Markdown file:
|
||||
|
||||
https://github.com/laurent22/joplin/blob/dev/readme/changelog_android.md
|
||||
|
||||
-->
|
||||
|
||||
<head>
|
||||
<title>Joplin Android app changelog | Joplin</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://joplinapp.org/css/bootstrap.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="https://joplinapp.org/favicon.ico">
|
||||
<!-- <link rel="stylesheet" href="https://joplinapp.org/css/fontawesome-all.min.css"> -->
|
||||
<link rel="stylesheet" href="https://joplinapp.org/css/fork-awesome.min.css">
|
||||
<script src="https://joplinapp.org/js/jquery-3.2.1.slim.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #F1F1F1;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.root {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a[href^="mailto:"] {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
td, th {
|
||||
padding: .8em;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.page-markdown table pre,
|
||||
.page-markdown table blockquote {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-markdown table pre,
|
||||
.page-markdown table blockquote {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-markdown table pre {
|
||||
background-color: rgba(0,0,0,0);
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
code {
|
||||
color: black;
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
font-size: .85em;
|
||||
/* word-break: break-all; */
|
||||
}
|
||||
pre code {
|
||||
border: none;
|
||||
}
|
||||
pre {
|
||||
font-size: .85em;
|
||||
}
|
||||
blockquote {
|
||||
font-size: 1em;
|
||||
color: #555;
|
||||
};
|
||||
#toc ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#toc > ul > li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#toc {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.title-icon {
|
||||
display: flex;
|
||||
height: 1em;
|
||||
}
|
||||
.title-text {
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
margin-bottom: .2em;
|
||||
margin-left: .5em;
|
||||
}
|
||||
.sub-title {
|
||||
font-weight: normal;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
box-shadow: 0 10px 20px #888888;
|
||||
}
|
||||
table.screenshots {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
table.screenshots th {
|
||||
height: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
table.screenshots th,
|
||||
table.screenshots td {
|
||||
border: 1px solid #C2C2C2;
|
||||
}
|
||||
img[align="left"] {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.mobile-screenshot {
|
||||
height: 40em;
|
||||
padding: 1em;
|
||||
}
|
||||
.cli-screenshot-wrapper {
|
||||
background-color: black;
|
||||
vertical-align: top;
|
||||
padding: 1em 2em 1em 1em;
|
||||
}
|
||||
.cli-screenshot {
|
||||
font-family: "Monaco", "Inconsolata", "CONSOLAS", "Deja Vu Sans Mono", "Droid Sans Mono", "Andale Mono", monospace;
|
||||
background-color: black;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
.cli-screenshot .prompt {
|
||||
color: #48C2F0;
|
||||
}
|
||||
.top-screenshot {
|
||||
margin-top: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.header {
|
||||
position: relative;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
color: white;
|
||||
background-color: #2B2B3D;
|
||||
}
|
||||
.header a h1 {
|
||||
color: white;
|
||||
}
|
||||
.header a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.content {
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-bottom: 2em;
|
||||
padding-top: 2em;
|
||||
}
|
||||
.forkme {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top:0;
|
||||
}
|
||||
.nav-wrapper {
|
||||
position: relative;
|
||||
width: inherit;
|
||||
}
|
||||
.nav {
|
||||
background-color: black;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.nav.sticky {
|
||||
position:fixed;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
box-shadow: 0 0 10px #000000;
|
||||
}
|
||||
.nav a {
|
||||
color: white;
|
||||
display: inline-block;
|
||||
padding: .6em .9em .6em .9em;
|
||||
}
|
||||
.nav ul {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 0;
|
||||
display: table-cell;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
/* For GSoC: */
|
||||
min-width: 470px;
|
||||
}
|
||||
.nav ul li {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
}
|
||||
.nav li.selected {
|
||||
background-color: #222;
|
||||
font-weight: bold;
|
||||
}
|
||||
.nav-right {
|
||||
display: flex;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
line-height: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.nav-right .share-btn {
|
||||
display: none;
|
||||
}
|
||||
.nav-right .small-share-btn {
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
padding: 2em;
|
||||
border-top: 1px solid #d4d4d4;
|
||||
margin-top: 2em;
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: 0.4em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.content{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#toc{
|
||||
display: block!important;
|
||||
align-self: flex-start;
|
||||
width: 300px;
|
||||
position: sticky; top: 20px; left: 0;
|
||||
}
|
||||
|
||||
.main{
|
||||
width: calc(100% - 300px);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #d4d4d4;
|
||||
margin-top: 30px;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
.nav-right .small-share-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container root page-changelog_android">
|
||||
|
||||
<div class="header">
|
||||
<a class="forkme" href="https://github.com/laurent22/joplin"><img src="https://joplinapp.org/images/ForkMe.png"/></a>
|
||||
<a href="https://joplinapp.org"><h1 class="title"><img class="title-icon" src="https://joplinapp.org/images/Icon512.png"><span class="title-text">Joplin</span></h1></a>
|
||||
<p class="sub-title">An open source note taking and to-do application with synchronisation capabilities</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-wrapper">
|
||||
<div class="nav">
|
||||
<ul>
|
||||
<li class=""><a href="https://joplinapp.org/" title="Home"><i class="fa fa-home"></i></a></li>
|
||||
<li><a href="https://discourse.joplinapp.org" title="Forum">Forum</a></li>
|
||||
<li><a class="gsoc" href="https://joplinapp.org/gsoc2021/index/" title="Google Summer of Code 2021">GSoC 2021</a></li>
|
||||
</ul>
|
||||
<div class="nav-right">
|
||||
<iframe class="share-btn share-btn-github" src="https://ghbtns.com/github-btn.html?user=laurent22&repo=joplin&type=star&count=true" frameborder="0" scrolling="0" width="115px" height="20px"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div id="toc"><ul>
|
||||
<li>
|
||||
<p>Applications</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/desktop/">Desktop application</a></li>
|
||||
<li><a href="https://joplinapp.org/mobile/">Mobile applications</a></li>
|
||||
<li><a href="https://joplinapp.org/terminal/">Terminal application</a></li>
|
||||
<li><a href="https://joplinapp.org/clipper/">Web Clipper</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Support</p>
|
||||
<ul>
|
||||
<li><a href="https://discourse.joplinapp.org">Joplin Forum</a></li>
|
||||
<li><a href="https://joplinapp.org/markdown/">Markdown Guide</a></li>
|
||||
<li><a href="https://joplinapp.org/e2ee/">How to enable end-to-end encryption</a></li>
|
||||
<li><a href="https://joplinapp.org/conflict/">What is a conflict?</a></li>
|
||||
<li><a href="https://joplinapp.org/debugging/">How to enable debug mode</a></li>
|
||||
<li><a href="https://joplinapp.org/rich_text_editor/">About the Rich Text editor limitations</a></li>
|
||||
<li><a href="https://joplinapp.org/faq/">FAQ</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Joplin API - Get Started</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/api/overview/">Joplin API Overview</a></li>
|
||||
<li><a href="https://joplinapp.org/api/get_started/plugins/">Plugin development</a></li>
|
||||
<li><a href="https://joplinapp.org/api/tutorials/toc_plugin/">Plugin tutorial</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Joplin API - References</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/api/references/plugin_api/classes/joplin.html">Plugin API</a></li>
|
||||
<li><a href="https://joplinapp.org/api/references/rest_api/">Data API</a></li>
|
||||
<li><a href="https://joplinapp.org/api/references/plugin_manifest/">Plugin manifest</a></li>
|
||||
<li><a href="https://joplinapp.org/api/references/plugin_loading_rules/">Plugin loading rules</a></li>
|
||||
<li><a href="https://joplinapp.org/api/references/plugin_theming/">Plugin theming</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Development</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/laurent22/joplin/blob/dev/BUILD.md">How to build the apps</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/e2ee/">End-to-end encryption spec</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/history/">Note History spec</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/sync_lock/">Sync Lock spec</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/plugins/">Plugin Architecture spec</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/search_sorting/">Search Sorting spec</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/server_file_url_format/">Server: File URL Format</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/server_delta_sync/">Server: Delta Sync</a></li>
|
||||
<li><a href="https://joplinapp.org/spec/server_sharing/">Server: Sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Google Summer of Code 2021</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/gsoc2021/index/">Google Summer of Code 2021</a></li>
|
||||
<li><a href="https://joplinapp.org/gsoc2021/pull_request_guidelines/">How to submit a GSoC pull request</a></li>
|
||||
<li><a href="https://joplinapp.org/gsoc2021/ideas/">Project Ideas</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>About</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/changelog/">Changelog (Desktop App)</a></li>
|
||||
<li><a href="https://joplinapp.org/changelog_cli/">Changelog (CLI App)</a></li>
|
||||
<li><a href="https://joplinapp.org/changelog_server/">Changelog (Server)</a></li>
|
||||
<li><a href="https://joplinapp.org/stats/">Stats</a></li>
|
||||
<li><a href="https://joplinapp.org/donate/">Donate</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate using PayPal"></a> <a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a> <a href="https://joplinapp.org/donate/#donations"><img src="https://joplinapp.org/images/badges/Donate-IBAN.svg" alt="Donate using IBAN"></a></p>
|
||||
<hr>
|
||||
<h1>Joplin Android app changelog<a name="joplin-android-app-changelog" href="#joplin-android-app-changelog" class="heading-anchor">🔗</a></h1>
|
||||
|
||||
<div class="bottom-links">
|
||||
<a href="https://github.com/laurent22/joplin/blob/dev/readme/changelog_android.md">
|
||||
<i class="fa fa-github"></i> Improve this doc
|
||||
</a>
|
||||
</div>
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
return; // Disabled
|
||||
|
||||
if ($(window).scrollTop() > 179) {
|
||||
$('.nav').addClass('sticky');
|
||||
} else {
|
||||
$('.nav').removeClass('sticky');
|
||||
}
|
||||
}
|
||||
|
||||
$(window).scroll(function() {
|
||||
stickyHeader();
|
||||
});
|
||||
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', 'UA-103586105-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
|
||||
</div></div>
|
||||
|
||||
<div class="footer">
|
||||
Copyright (C) 2016-2021 Laurent Cozic
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -405,6 +405,16 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog_server.md
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate using PayPal"></a> <a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a> <a href="https://joplinapp.org/donate/#donations"><img src="https://joplinapp.org/images/badges/Donate-IBAN.svg" alt="Donate using IBAN"></a></p>
|
||||
<hr>
|
||||
<h1>Joplin Server Changelog<a name="joplin-server-changelog" href="#joplin-server-changelog" class="heading-anchor">🔗</a></h1>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/server-v2.0.1">server-v2.0.1</a> (Pre-release) - 2021-05-14T13:55:45Z<a name="server-v2-0-1-https-github-com-laurent22-joplin-releases-tag-server-v2-0-1-pre-release-2021-05-14t13-55-45z" href="#server-v2-0-1-https-github-com-laurent22-joplin-releases-tag-server-v2-0-1-pre-release-2021-05-14t13-55-45z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>New: Add support for sharing notes via a link (ccbc329)</li>
|
||||
<li>New: Add support for sharing a folder (#4772)</li>
|
||||
<li>New: Added log page to view latest changes to files (874f301)</li>
|
||||
<li>Fixed: Prevent new user password from being hashed twice (76c143e)</li>
|
||||
<li>Fixed: Fixed crash when rendering note with links to non-existing resources or notes (07484de)</li>
|
||||
<li>Fixed: Fixed error handling when no session is provided (63a5bfa)</li>
|
||||
<li>Fixed: Fixed uploading empty file to the API (#4402)</li>
|
||||
</ul>
|
||||
<h2><a href="https://github.com/laurent22/joplin/releases/tag/server-v1.7.2">server-v1.7.2</a> - 2021-01-24T19:11:10Z<a name="server-v1-7-2-https-github-com-laurent22-joplin-releases-tag-server-v1-7-2-2021-01-24t19-11-10z" href="#server-v1-7-2-https-github-com-laurent22-joplin-releases-tag-server-v1-7-2-2021-01-24t19-11-10z" class="heading-anchor">🔗</a></h2>
|
||||
<ul>
|
||||
<li>Fixed: Fixed password hashing when changing password</li>
|
||||
|
@@ -424,19 +424,19 @@ https://github.com/laurent22/joplin/blob/dev/README.md
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows (32 and 64-bit)</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-Setup-1.7.11.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-Setup-1.8.5.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-1.7.11.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-1.8.5.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-1.7.11.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-1.8.5.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>On Windows</strong>, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/JoplinPortable.exe'>Portable version</a>. The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.</p>
|
||||
<p><strong>On Windows</strong>, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/JoplinPortable.exe'>Portable version</a>. The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.</p>
|
||||
<p><strong>On Linux</strong>, the recommended way is to use the following installation script as it will handle the desktop icon too:</p>
|
||||
<pre><code style="word-break: break-all">wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash</code></pre>
|
||||
<h2>Mobile applications<a name="mobile-applications" href="#mobile-applications" class="heading-anchor">🔗</a></h2>
|
||||
@@ -662,7 +662,7 @@ Joplin is also capable of exporting to a number of other formats including HTML
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteObjectVersion",
|
||||
"s3:PutObject"
|
||||
]
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::joplin-bucket",
|
||||
"arn:aws:s3:::joplin-bucket/*"
|
||||
@@ -692,7 +692,7 @@ Joplin is also capable of exporting to a number of other formats including HTML
|
||||
<ul>
|
||||
<li><strong>Windows</strong>: >= 8. Make sure the Action Center is enabled on Windows. Task bar balloon for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.</li>
|
||||
<li><strong>macOS</strong>: >= 10.8 or Growl if earlier.</li>
|
||||
<li><strong>Linux</strong>: <code>notify-osd</code> or <code>libnotify-bin</code> installed (Ubuntu should have this by default). Growl otherwise</li>
|
||||
<li><strong>Linux</strong>: <code>notify-send</code> tool, delivered through packages <code>notify-osd</code>, <code>libnotify-bin</code> or <code>libnotify-tools</code>. GNOME should have this by default, but install <code>libnotify-tools</code> if using KDE Plasma.</li>
|
||||
</ul>
|
||||
<p>See <a href="https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>On mobile, the alarms will be displayed using the built-in notification system.</p>
|
||||
@@ -989,49 +989,49 @@ Eg. <code>:search -- "-tag:tag1"</code>.</p>
|
||||
<td>Arabic</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po">ar</a></td>
|
||||
<td><a href="mailto:Whaell@protonmail.com">Whaell O</a></td>
|
||||
<td>99%</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/es/basque_country.png" alt=""></td>
|
||||
<td>Basque</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po">eu</a></td>
|
||||
<td>juan.abasolo@ehu.eus</td>
|
||||
<td>31%</td>
|
||||
<td>30%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ba.png" alt=""></td>
|
||||
<td>Bosnian (Bosna i Hercegovina)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po">bs_BA</a></td>
|
||||
<td><a href="mailto:dervis.t@pm.me">Derviš T.</a></td>
|
||||
<td>74%</td>
|
||||
<td>75%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/bg.png" alt=""></td>
|
||||
<td>Bulgarian (България)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po">bg_BG</a></td>
|
||||
<td></td>
|
||||
<td>60%</td>
|
||||
<td>58%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/es/catalonia.png" alt=""></td>
|
||||
<td>Catalan</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po">ca</a></td>
|
||||
<td>jmontane, 2019</td>
|
||||
<td>85%</td>
|
||||
<td>83%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/hr.png" alt=""></td>
|
||||
<td>Croatian (Hrvatska)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po">hr_HR</a></td>
|
||||
<td><a href="mailto:mail@milotype.de">Milo Ivir</a></td>
|
||||
<td>99%</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/cz.png" alt=""></td>
|
||||
<td>Czech (Česká republika)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po">cs_CZ</a></td>
|
||||
<td><a href="mailto:lukas@aiya.cz">Lukas Helebrandt</a></td>
|
||||
<td>89%</td>
|
||||
<td>86%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/dk.png" alt=""></td>
|
||||
@@ -1045,14 +1045,14 @@ Eg. <code>:search -- "-tag:tag1"</code>.</p>
|
||||
<td>Deutsch (Deutschland)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po">de_DE</a></td>
|
||||
<td><a href="mailto:atalanttore@googlemail.com">Atalanttore</a></td>
|
||||
<td>98%</td>
|
||||
<td>95%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ee.png" alt=""></td>
|
||||
<td>Eesti Keel (Eesti)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po">et_EE</a></td>
|
||||
<td></td>
|
||||
<td>58%</td>
|
||||
<td>57%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/gb.png" alt=""></td>
|
||||
@@ -1073,203 +1073,203 @@ Eg. <code>:search -- "-tag:tag1"</code>.</p>
|
||||
<td>Español (España)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po">es_ES</a></td>
|
||||
<td><a href="mailto:mario.campo@gmail.com">Mario Campo</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/esperanto.png" alt=""></td>
|
||||
<td>Esperanto</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po">eo</a></td>
|
||||
<td>Marton Paulo</td>
|
||||
<td>34%</td>
|
||||
<td>33%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/fi.png" alt=""></td>
|
||||
<td>Finnish (Suomi)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po">fi_FI</a></td>
|
||||
<td>mrkaato</td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/fr.png" alt=""></td>
|
||||
<td>Français (France)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po">fr_FR</a></td>
|
||||
<td>Laurent Cozic</td>
|
||||
<td>95%</td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/es/galicia.png" alt=""></td>
|
||||
<td>Galician (España)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po">gl_ES</a></td>
|
||||
<td><a href="mailto:marcoslansgarza@gmail.com">Marcos Lans</a></td>
|
||||
<td>39%</td>
|
||||
<td>38%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/id.png" alt=""></td>
|
||||
<td>Indonesian (Indonesia)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po">id_ID</a></td>
|
||||
<td><a href="mailto:42007357+eresytter@users.noreply.github.com">eresytter</a></td>
|
||||
<td>96%</td>
|
||||
<td>93%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/it.png" alt=""></td>
|
||||
<td>Italiano (Italia)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po">it_IT</a></td>
|
||||
<td><a href="mailto:mailfilledwithspam@gmail.com">Alessandro Bernardello</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/hu.png" alt=""></td>
|
||||
<td>Magyar (Magyarország)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po">hu_HU</a></td>
|
||||
<td><a href="mailto:mail@szokesandor.hu">Szőke Sándor</a></td>
|
||||
<td>91%</td>
|
||||
<td>88%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/be.png" alt=""></td>
|
||||
<td>Nederlands (België, Belgique, Belgien)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po">nl_BE</a></td>
|
||||
<td></td>
|
||||
<td>95%</td>
|
||||
<td>92%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/nl.png" alt=""></td>
|
||||
<td>Nederlands (Nederland)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po">nl_NL</a></td>
|
||||
<td><a href="mailto:metbril@users.noreply.github.com">MetBril</a></td>
|
||||
<td>98%</td>
|
||||
<td>95%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/no.png" alt=""></td>
|
||||
<td>Norwegian (Norge, Noreg)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po">nb_NO</a></td>
|
||||
<td><a href="mailto:code@mxe.no">Mats Estensen</a></td>
|
||||
<td>78%</td>
|
||||
<td>76%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ir.png" alt=""></td>
|
||||
<td>Persian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po">fa</a></td>
|
||||
<td><a href="mailto:kourox@protonmail.com">Kourosh Firoozbakht</a></td>
|
||||
<td>74%</td>
|
||||
<td>71%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/pl.png" alt=""></td>
|
||||
<td>Polski (Polska)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po">pl_PL</a></td>
|
||||
<td><a href="mailto:hello.konhi@gmail.com">konhi</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/br.png" alt=""></td>
|
||||
<td>Português (Brasil)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po">pt_BR</a></td>
|
||||
<td><a href="mailto:nicolas.suzuki@pm.me">Nicolas Suzuki</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/pt.png" alt=""></td>
|
||||
<td>Português (Portugal)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po">pt_PT</a></td>
|
||||
<td><a href="mailto:dcaveiro@yahoo.com">Diogo Caveiro</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ro.png" alt=""></td>
|
||||
<td>Română</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po">ro</a></td>
|
||||
<td><a href="mailto:cristi.duluta@gmail.com">Cristi Duluta</a></td>
|
||||
<td>68%</td>
|
||||
<td>66%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/si.png" alt=""></td>
|
||||
<td>Slovenian (Slovenija)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po">sl_SI</a></td>
|
||||
<td><a href="mailto:martin.korelic@protonmail.com">Martin Korelič</a></td>
|
||||
<td>99%</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/se.png" alt=""></td>
|
||||
<td>Svenska</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po">sv</a></td>
|
||||
<td><a href="mailto:jonatan@autistici.org">Jonatan Nyberg</a></td>
|
||||
<td>63%</td>
|
||||
<td>61%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/th.png" alt=""></td>
|
||||
<td>Thai (ประเทศไทย)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po">th_TH</a></td>
|
||||
<td></td>
|
||||
<td>47%</td>
|
||||
<td>45%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/vi.png" alt=""></td>
|
||||
<td>Tiếng Việt</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po">vi</a></td>
|
||||
<td></td>
|
||||
<td>75%</td>
|
||||
<td>73%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/tr.png" alt=""></td>
|
||||
<td>Türkçe (Türkiye)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po">tr_TR</a></td>
|
||||
<td><a href="mailto:arda@kilicdagi.com">Arda Kılıçdağı</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ua.png" alt=""></td>
|
||||
<td>Ukrainian (Україна)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po">uk_UA</a></td>
|
||||
<td><a href="mailto:vandreykiv@gmail.com">Vyacheslav Andreykiv</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/gr.png" alt=""></td>
|
||||
<td>Ελληνικά (Ελλάδα)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po">el_GR</a></td>
|
||||
<td><a href="mailto:xaris@tuta.io">Harris Arvanitis</a></td>
|
||||
<td>85%</td>
|
||||
<td>97%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ru.png" alt=""></td>
|
||||
<td>Русский (Россия)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po">ru_RU</a></td>
|
||||
<td><a href="mailto:thesermanarm@gmail.com">Sergey Segeda</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/rs.png" alt=""></td>
|
||||
<td>српски језик (Србија)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po">sr_RS</a></td>
|
||||
<td></td>
|
||||
<td>73%</td>
|
||||
<td>71%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/cn.png" alt=""></td>
|
||||
<td>中文 (简体)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po">zh_CN</a></td>
|
||||
<td><a href="mailto:zyangmath@gmail.com">Yang Zhang</a></td>
|
||||
<td>97%</td>
|
||||
<td>94%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/tw.png" alt=""></td>
|
||||
<td>中文 (繁體)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po">zh_TW</a></td>
|
||||
<td><a href="mailto:yaozeye@yahoo.co.jp">Yaoze Ye</a></td>
|
||||
<td>95%</td>
|
||||
<td>92%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/jp.png" alt=""></td>
|
||||
<td>日本語 (日本)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po">ja_JP</a></td>
|
||||
<td><a href="mailto:genneko217@gmail.com">genneko</a></td>
|
||||
<td>98%</td>
|
||||
<td>97%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/kr.png" alt=""></td>
|
||||
<td>한국어</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po">ko</a></td>
|
||||
<td><a href="mailto:potatogim@potatogim.net">Ji-Hyeon Gim</a></td>
|
||||
<td>97%</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -101,15 +101,10 @@
|
||||
"default": "",
|
||||
"description": "Joplin Server URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.9.directory": {
|
||||
"type": "string",
|
||||
"default": "Apps/Joplin",
|
||||
"description": "Joplin Server Directory"
|
||||
},
|
||||
"sync.9.username": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Joplin Server username"
|
||||
"description": "Joplin Server email"
|
||||
},
|
||||
"sync.9.password": {
|
||||
"type": "string",
|
||||
@@ -602,6 +597,11 @@
|
||||
"default": -1,
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.userId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"style.zoom": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
@@ -633,7 +633,7 @@
|
||||
},
|
||||
"autoUpdateEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"default": true,
|
||||
"description": "Automatically update the application"
|
||||
},
|
||||
"autoUpdate.includePreReleases": {
|
||||
@@ -726,12 +726,6 @@
|
||||
"description": "Enable spell checking in Markdown editor? (WARNING BETA feature). Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)",
|
||||
"$comment": "private"
|
||||
},
|
||||
"image.noresizing": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not resize images",
|
||||
"$comment": "private"
|
||||
},
|
||||
"net.customCertificates": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,7 @@ module.exports = {
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/tests/support/',
|
||||
'<rootDir>/build/',
|
||||
'<rootDir>/tests/test-utils.js',
|
||||
'<rootDir>/tests/test-utils-synchronizer.js',
|
||||
'<rootDir>/tests/testUtils.js',
|
||||
'<rootDir>/tests/tmp/',
|
||||
'<rootDir>/tests/test data/',
|
||||
],
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import KeychainService from '../../services/keychain/KeychainService';
|
||||
import shim from '../../shim';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
const { db, setupDatabaseAndSynchronizer, switchClient } = require('../../testing/test-utils.js');
|
||||
import KeychainService from '@joplin/lib/services/keychain/KeychainService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { db, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
function describeIfCompatible(name: string, fn: any, elseFn: any) {
|
||||
if (['win32', 'darwin'].includes(shim.platformName())) {
|
@@ -7,8 +7,8 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import * as fs from 'fs-extra';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { newPluginScript } from '@joplin/lib/testing/test-utils';
|
||||
import { expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir, supportDir } from '@joplin/lib/testing/test-utils';
|
||||
import { newPluginScript } from '../../testUtils';
|
||||
|
||||
const testPluginDir = `${supportDir}/plugins`;
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir }
|
||||
|
||||
async function newRepoApi(): Promise<RepositoryApi> {
|
||||
const repo = new RepositoryApi(`${supportDir}/pluginRepo`, await createTempDir());
|
||||
await repo.loadManifests();
|
||||
await repo.initialize();
|
||||
return repo;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
const { waitForFolderCount, newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } = require('@joplin/lib/testing/test-utils');
|
||||
import { waitForFolderCount, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
|
||||
describe('JoplinSettings', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('JoplinSettings', () => {
|
||||
});
|
||||
|
||||
test('should listen to setting change event', async () => {
|
||||
const service = new newPluginService() as PluginService;
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
@@ -68,7 +68,7 @@ describe('JoplinSettings', () => {
|
||||
});
|
||||
|
||||
test('should allow registering multiple settings', async () => {
|
||||
const service = new newPluginService() as PluginService;
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
const { newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } = require('@joplin/lib/testing/test-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
|
||||
describe('JoplinViewMenuItem', () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('JoplinViewMenuItem', () => {
|
||||
});
|
||||
|
||||
test('should register commands with the keymap service', async () => {
|
||||
const service = new newPluginService() as PluginService;
|
||||
const service = newPluginService();
|
||||
|
||||
KeymapService.instance().initialize();
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
|
||||
describe('JoplinWorkspace', () => {
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import sandboxProxy, { Target } from '@joplin/lib/services/plugins/sandboxProxy';
|
||||
|
||||
const { setupDatabaseAndSynchronizer, switchClient } = require('@joplin/lib/testing/test-utils.js');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
describe('services_plugins_sandboxProxy', function() {
|
||||
|
||||
|
41
packages/app-cli/tests/testUtils.ts
Normal file
41
packages/app-cli/tests/testUtils.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import PluginRunner from '../app/services/plugins/PluginRunner';
|
||||
|
||||
export interface PluginServiceOptions {
|
||||
getState?(): Record<string, any>;
|
||||
}
|
||||
|
||||
export function newPluginService(appVersion = '1.4', options: PluginServiceOptions = null): PluginService {
|
||||
options = options || {};
|
||||
|
||||
const runner = new PluginRunner();
|
||||
const service = new PluginService();
|
||||
service.initialize(
|
||||
appVersion,
|
||||
{
|
||||
joplin: {},
|
||||
},
|
||||
runner,
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: options.getState ? options.getState : () => {},
|
||||
}
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
export function newPluginScript(script: string) {
|
||||
return `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.PluginTest",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
|
||||
${script}
|
||||
`;
|
||||
}
|
@@ -113,7 +113,7 @@ export default function(props: Props) {
|
||||
|
||||
let loadError: Error = null;
|
||||
try {
|
||||
await repoApi().loadManifests();
|
||||
await repoApi().initialize();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
loadError = error;
|
||||
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import useWindowResizeEvent from './utils/useWindowResizeEvent';
|
||||
import setLayoutItemProps from './utils/setLayoutItemProps';
|
||||
import useLayoutItemSizes, { LayoutItemSizes, itemSize } from './utils/useLayoutItemSizes';
|
||||
import useLayoutItemSizes, { LayoutItemSizes, itemSize, calculateMaxSizeAvailableForItem, itemMinWidth, itemMinHeight } from './utils/useLayoutItemSizes';
|
||||
import validateLayout from './utils/validateLayout';
|
||||
import { Size, LayoutItem } from './utils/types';
|
||||
import { canMove, MoveDirection } from './utils/movements';
|
||||
@@ -11,9 +11,6 @@ import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootWrapper, MoveModeRoot
|
||||
import { Resizable } from 're-resizable';
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const itemMinWidth = 20;
|
||||
const itemMinHeight = 20;
|
||||
|
||||
interface onResizeEvent {
|
||||
layout: LayoutItem;
|
||||
}
|
||||
@@ -35,7 +32,7 @@ function itemVisible(item: LayoutItem, moveMode: boolean) {
|
||||
return item.visible !== false;
|
||||
}
|
||||
|
||||
function renderContainer(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, onResizeStart: Function, onResize: Function, onResizeStop: Function, children: any[], isLastChild: boolean, moveMode: boolean): any {
|
||||
function renderContainer(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, resizedItemMaxSize: Size | null, onResizeStart: Function, onResize: Function, onResizeStop: Function, children: any[], isLastChild: boolean, moveMode: boolean): any {
|
||||
const style: any = {
|
||||
display: itemVisible(item, moveMode) ? 'flex' : 'none',
|
||||
flexDirection: item.direction,
|
||||
@@ -68,6 +65,8 @@ function renderContainer(item: LayoutItem, parent: LayoutItem | null, sizes: Lay
|
||||
enable={enable}
|
||||
minWidth={'minWidth' in item ? item.minWidth : itemMinWidth}
|
||||
minHeight={'minHeight' in item ? item.minHeight : itemMinHeight}
|
||||
maxWidth={resizedItemMaxSize?.width}
|
||||
maxHeight={resizedItemMaxSize?.height}
|
||||
>
|
||||
{children}
|
||||
</Resizable>
|
||||
@@ -114,6 +113,7 @@ function ResizableLayout(props: Props) {
|
||||
key: item.key,
|
||||
initialWidth: sizes[item.key].width,
|
||||
initialHeight: sizes[item.key].height,
|
||||
maxSize: calculateMaxSizeAvailableForItem(item, parent, sizes),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ function ResizableLayout(props: Props) {
|
||||
setResizedItem(null);
|
||||
}
|
||||
|
||||
const resizedItemMaxSize = item.key === resizedItem?.key ? resizedItem.maxSize : null;
|
||||
if (!item.children) {
|
||||
const size = itemSize(item, parent, sizes, false);
|
||||
|
||||
@@ -155,7 +156,7 @@ function ResizableLayout(props: Props) {
|
||||
|
||||
const wrapper = renderItemWrapper(comp, item, parent, size, props.moveMode);
|
||||
|
||||
return renderContainer(item, parent, sizes, onResizeStart, onResize, onResizeStop, [wrapper], isLastChild, props.moveMode);
|
||||
return renderContainer(item, parent, sizes, resizedItemMaxSize, onResizeStart, onResize, onResizeStop, [wrapper], isLastChild, props.moveMode);
|
||||
} else {
|
||||
const childrenComponents = [];
|
||||
for (let i = 0; i < item.children.length; i++) {
|
||||
@@ -163,7 +164,7 @@ function ResizableLayout(props: Props) {
|
||||
childrenComponents.push(renderLayoutItem(child, item, sizes, isVisible && itemVisible(child, props.moveMode), i === item.children.length - 1));
|
||||
}
|
||||
|
||||
return renderContainer(item, parent, sizes, onResizeStart, onResize, onResizeStop, childrenComponents, isLastChild, props.moveMode);
|
||||
return renderContainer(item, parent, sizes, resizedItemMaxSize, onResizeStart, onResize, onResizeStop, childrenComponents, isLastChild, props.moveMode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import useLayoutItemSizes, { itemSize } from './useLayoutItemSizes';
|
||||
import useLayoutItemSizes, { itemSize, calculateMaxSizeAvailableForItem } from './useLayoutItemSizes';
|
||||
import { LayoutItem, LayoutItemDirection } from './types';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import validateLayout from './validateLayout';
|
||||
@@ -138,4 +138,219 @@ describe('useLayoutItemSizes', () => {
|
||||
expect(itemSize(parent.children[1], parent, sizes, false)).toEqual({ width: 95, height: 50 });
|
||||
});
|
||||
|
||||
test('should decrease size of the largest item if the total size would be larger than the container', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
key: 'col3',
|
||||
minWidth: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
|
||||
expect(sizes.col1.width).toBe(50);
|
||||
expect(sizes.col2.width).toBe(100);
|
||||
expect(sizes.col3.width).toBe(50);
|
||||
});
|
||||
|
||||
test('should not allow a minWidth of 0, should still make space for the item', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 210,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
minWidth: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
|
||||
expect(sizes.col1.width).toBe(160);
|
||||
expect(sizes.col2.width).toBe(40); // default minWidth is 40
|
||||
});
|
||||
|
||||
test('should ignore invisible items when counting remaining size', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 110,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
key: 'col3',
|
||||
minWidth: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
|
||||
expect(sizes.col1.width).toBe(0);
|
||||
expect(sizes.col2.width).toBe(100);
|
||||
expect(sizes.col3.width).toBe(100);
|
||||
});
|
||||
|
||||
test('should ignore invisible items when selecting largest child', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 110,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
key: 'col3',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
key: 'col4',
|
||||
minWidth: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
|
||||
expect(sizes.col1.width).toBe(0);
|
||||
expect(sizes.col2.width).toBe(100);
|
||||
expect(sizes.col3.width).toBe(50);
|
||||
expect(sizes.col4.width).toBe(50);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('calculateMaxSizeAvailableForItem', () => {
|
||||
|
||||
test('should give maximum available space this item can take up during resizing', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
key: 'col3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
const maxSize1 = calculateMaxSizeAvailableForItem(layout.children[0], layout, sizes);
|
||||
const maxSize2 = calculateMaxSizeAvailableForItem(layout.children[1], layout, sizes);
|
||||
|
||||
// maxSize = totalSize - ( [size of items with set size, except for the current item] + [minimum size of items with no set size] )
|
||||
expect(maxSize1.width).toBe(90); // 90 = layout.width - (col2.width + col3.minWidth(=40) )
|
||||
expect(maxSize2.width).toBe(110); // 110 = layout.width - (col1.width + col3.minWidth(=40) )
|
||||
});
|
||||
|
||||
test('should respect minimum sizes', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
key: 'col3',
|
||||
minWidth: 60,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
const maxSize1 = calculateMaxSizeAvailableForItem(layout.children[0], layout, sizes);
|
||||
const maxSize2 = calculateMaxSizeAvailableForItem(layout.children[1], layout, sizes);
|
||||
|
||||
// maxSize = totalSize - ( [size of items with set size, except for the current item] + [minimum size of items with no set size] )
|
||||
expect(maxSize1.width).toBe(70); // 70 = layout.width - (col2.width + col3.minWidth)
|
||||
expect(maxSize2.width).toBe(90); // 90 = layout.width - (col1.width + col3.minWidth)
|
||||
});
|
||||
|
||||
test('should not allow a minWidth of 0, should still leave space for the item', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 200,
|
||||
height: 100,
|
||||
direction: LayoutItemDirection.Row,
|
||||
children: [
|
||||
{
|
||||
key: 'col1',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
key: 'col2',
|
||||
minWidth: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLayoutItemSizes(layout));
|
||||
const sizes = result.current;
|
||||
const maxSize1 = calculateMaxSizeAvailableForItem(layout.children[0], layout, sizes);
|
||||
|
||||
// maxSize = totalSize - ( [size of items with set size, except for the current item] + [minimum size of items with no set size] )
|
||||
expect(maxSize1.width).toBe(160); // 160 = layout.width - col2.minWidth(=40)
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -3,6 +3,9 @@ import { LayoutItem, Size } from './types';
|
||||
|
||||
const dragBarThickness = 5;
|
||||
|
||||
export const itemMinWidth = 40;
|
||||
export const itemMinHeight = 40;
|
||||
|
||||
export interface LayoutItemSizes {
|
||||
[key: string]: Size;
|
||||
}
|
||||
@@ -17,8 +20,8 @@ export function itemSize(item: LayoutItem, parent: LayoutItem | null, sizes: Lay
|
||||
const bottomGap = !isContainer && (item.resizableBottom || parentResizableBottom) ? dragBarThickness : 0;
|
||||
|
||||
return {
|
||||
width: ('width' in item ? item.width : sizes[item.key].width) - rightGap,
|
||||
height: ('height' in item ? item.height : sizes[item.key].height) - bottomGap,
|
||||
width: sizes[item.key].width - rightGap,
|
||||
height: sizes[item.key].height - bottomGap,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +41,10 @@ function calculateChildrenSizes(item: LayoutItem, parent: LayoutItem | null, siz
|
||||
const noWidthChildren: any[] = [];
|
||||
const noHeightChildren: any[] = [];
|
||||
|
||||
// The minimum space required for items with no defined size
|
||||
let noWidthChildrenMinWidth = 0;
|
||||
let noHeightChildrenMinHeight = 0;
|
||||
|
||||
for (const child of item.children) {
|
||||
let w = 'width' in child ? child.width : null;
|
||||
let h = 'height' in child ? child.height : null;
|
||||
@@ -47,10 +54,43 @@ function calculateChildrenSizes(item: LayoutItem, parent: LayoutItem | null, siz
|
||||
}
|
||||
|
||||
sizes[child.key] = { width: w, height: h };
|
||||
|
||||
if (w !== null) remainingSize.width -= w;
|
||||
if (h !== null) remainingSize.height -= h;
|
||||
if (w === null) noWidthChildren.push({ item: child, parent: item });
|
||||
if (h === null) noHeightChildren.push({ item: child, parent: item });
|
||||
if (w === null) {
|
||||
noWidthChildren.push({ item: child, parent: item });
|
||||
noWidthChildrenMinWidth += child.minWidth || itemMinWidth;
|
||||
}
|
||||
if (h === null) {
|
||||
noHeightChildren.push({ item: child, parent: item });
|
||||
noHeightChildrenMinHeight += child.minHeight || itemMinHeight;
|
||||
}
|
||||
}
|
||||
|
||||
while (remainingSize.width < noWidthChildrenMinWidth) {
|
||||
// There is not enough space, the widest item will be made smaller
|
||||
let widestChild = item.children[0].key;
|
||||
for (const child of item.children) {
|
||||
if (!child.visible) continue;
|
||||
if (sizes[child.key].width > sizes[widestChild].width) widestChild = child.key;
|
||||
}
|
||||
|
||||
const dw = Math.abs(remainingSize.width - noWidthChildrenMinWidth);
|
||||
sizes[widestChild].width -= dw;
|
||||
remainingSize.width += dw;
|
||||
}
|
||||
|
||||
while (remainingSize.height < noHeightChildrenMinHeight) {
|
||||
// There is not enough space, the tallest item will be made smaller
|
||||
let tallestChild = item.children[0].key;
|
||||
for (const child of item.children) {
|
||||
if (!child.visible) continue;
|
||||
if (sizes[child.key].height > sizes[tallestChild].height) tallestChild = child.key;
|
||||
}
|
||||
|
||||
const dh = Math.abs(remainingSize.height - noHeightChildrenMinHeight);
|
||||
sizes[tallestChild].height -= dh;
|
||||
remainingSize.height += dh;
|
||||
}
|
||||
|
||||
if (noWidthChildren.length) {
|
||||
@@ -77,6 +117,24 @@ function calculateChildrenSizes(item: LayoutItem, parent: LayoutItem | null, siz
|
||||
return sizes;
|
||||
}
|
||||
|
||||
// Gives the maximum available space for this item that it can take up during resizing
|
||||
// availableSize = totalSize - ( [size of items with set size, except for the current item] + [minimum size of items with no set size] )
|
||||
export function calculateMaxSizeAvailableForItem(item: LayoutItem, parent: LayoutItem, sizes: LayoutItemSizes): Size {
|
||||
const availableSize: Size = { ...sizes[parent.key] };
|
||||
|
||||
for (const sibling of parent.children) {
|
||||
if (!sibling.visible) continue;
|
||||
|
||||
availableSize.width -= 'width' in sibling ? sizes[sibling.key].width : (sibling.minWidth || itemMinWidth);
|
||||
availableSize.height -= 'height' in sibling ? sizes[sibling.key].height : (sibling.minHeight || itemMinHeight);
|
||||
}
|
||||
|
||||
availableSize.width += sizes[item.key].width;
|
||||
availableSize.height += sizes[item.key].height;
|
||||
|
||||
return availableSize;
|
||||
}
|
||||
|
||||
export default function useLayoutItemSizes(layout: LayoutItem, makeAllVisible: boolean = false) {
|
||||
return useMemo(() => {
|
||||
let sizes: LayoutItemSizes = {};
|
||||
|
@@ -449,7 +449,11 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
|
||||
renderTag(tag: any, selected: boolean) {
|
||||
const anchorRef = this.anchorItemRef('tag', tag.id);
|
||||
const noteCount = Setting.value('showNoteCounts') ? this.renderNoteCount(tag.note_count) : '';
|
||||
let noteCount = null;
|
||||
if (Setting.value('showNoteCounts')) {
|
||||
if (Setting.value('showCompletedTodos')) noteCount = this.renderNoteCount(tag.note_count);
|
||||
else noteCount = this.renderNoteCount(tag.note_count - tag.todo_completed_count);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledListItem selected={selected}
|
||||
|
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -26,12 +26,12 @@ if [ "$RESET_ALL" == "1" ]; then
|
||||
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
echo "config sync.target 9" >> "$CMD_FILE"
|
||||
echo "config sync.9.path http://localhost:22300" >> "$CMD_FILE"
|
||||
echo "config sync.9.path http://api-joplincloud.local:22300" >> "$CMD_FILE"
|
||||
echo "config sync.9.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.9.password 123456" >> "$CMD_FILE"
|
||||
|
||||
if [ "$1" == "1" ]; then
|
||||
curl --data '{"action": "createTestUsers"}' http://localhost:22300/api/debug
|
||||
if [ "$USER_NUM" == "1" ]; then
|
||||
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api-joplincloud.local:22300/api/debug
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
echo 'mkbook "other"' >> "$CMD_FILE"
|
||||
|
10
packages/app-mobile/package-lock.json
generated
10
packages/app-mobile/package-lock.json
generated
@@ -4905,6 +4905,11 @@
|
||||
"resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.6.tgz",
|
||||
"integrity": "sha512-JNAkmPeB/GS2tCRqUzRPsTOHpGDah7xP18vGJfIjZC+W2sxEHbxgJxetIjIqhjQ3yYbYNEELkM/spKLtwoOSUQ=="
|
||||
},
|
||||
"joplin-rn-alarm-notification": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/joplin-rn-alarm-notification/-/joplin-rn-alarm-notification-1.0.3.tgz",
|
||||
"integrity": "sha512-HZGDrLmYf6aMVgzk02w4DS9CjaTogE1hnOLdMDsrWkZzRskO6g3bZw+Bwlc63cCX4ZLZeeWIaABzHoWKAbLzpQ=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -7097,11 +7102,6 @@
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-native-alarm-notification": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-alarm-notification/-/react-native-alarm-notification-1.7.1.tgz",
|
||||
"integrity": "sha512-cvfSqCCfw48NyeFTEL5WOF/tkeWLNI7X1mVoEQ/9aY+2fuBtkCfZUoJ7vvOOHeryPbDJrlDNpRWTi3erLphZ+w=="
|
||||
},
|
||||
"react-native-camera": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-3.40.0.tgz",
|
||||
|
2
packages/fork-htmlparser2/package-lock.json
generated
2
packages/fork-htmlparser2/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"version": "4.1.24",
|
||||
"version": "4.1.26",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.24",
|
||||
"version": "4.1.26",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
2
packages/fork-sax/package-lock.json
generated
2
packages/fork-sax/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-sax",
|
||||
"version": "1.2.28",
|
||||
"version": "1.2.30",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.28",
|
||||
"version": "1.2.30",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -343,7 +343,7 @@ export default class JoplinDatabase extends Database {
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
// Note: v16 and v17 don't do anything. They were used to debug an issue.
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
|
||||
@@ -876,6 +876,18 @@ export default class JoplinDatabase extends Database {
|
||||
queries.push('ALTER TABLE resources ADD COLUMN share_id TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
if (targetVersion == 38) {
|
||||
queries.push('DROP VIEW tags_with_note_count');
|
||||
queries.push(`CREATE VIEW tags_with_note_count AS
|
||||
SELECT tags.id as id, tags.title as title, tags.created_time as created_time, tags.updated_time as updated_time, COUNT(notes.id) as note_count,
|
||||
SUM(CASE WHEN notes.todo_completed > 0 THEN 1 ELSE 0 END) AS todo_completed_count
|
||||
FROM tags
|
||||
LEFT JOIN note_tags nt on nt.tag_id = tags.id
|
||||
LEFT JOIN notes on notes.id = nt.note_id
|
||||
WHERE notes.id IS NOT NULL
|
||||
GROUP BY tags.id`);
|
||||
}
|
||||
|
||||
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };
|
||||
|
||||
queries.push(updateVersionQuery);
|
||||
|
@@ -1,13 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class JoplinError extends Error {
|
||||
constructor(message, code = null, details = null) {
|
||||
super(message);
|
||||
this.code = null;
|
||||
this.details = '';
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
exports.default = JoplinError;
|
||||
//# sourceMappingURL=JoplinError.js.map
|
@@ -3,8 +3,11 @@ import { _ } from './locale';
|
||||
const { rtrimSlashes } = require('./path-utils.js');
|
||||
import JoplinError from './JoplinError';
|
||||
import { Env } from './models/Setting';
|
||||
import Logger from './Logger';
|
||||
const { stringify } = require('query-string');
|
||||
|
||||
const logger = Logger.create('JoplinServerApi');
|
||||
|
||||
interface Options {
|
||||
baseUrl(): string;
|
||||
username(): string;
|
||||
@@ -133,71 +136,78 @@ export default class JoplinServerApi {
|
||||
url += stringify(query);
|
||||
}
|
||||
|
||||
let response: any = null;
|
||||
|
||||
if (this.debugRequests_) {
|
||||
console.info('Joplin API Call', `${method} ${url}`, headers, options);
|
||||
console.info(this.requestToCurl_(url, fetchOptions));
|
||||
}
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
if (fetchOptions.path) {
|
||||
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
|
||||
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
|
||||
}
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
if (this.debugRequests_) {
|
||||
console.info('Joplin API Response', responseText);
|
||||
}
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message: string, code: number = 0) => {
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
// return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
return new JoplinError(message, code, `${method} ${path}: ${message} (${code}): ${shortResponseText}`);
|
||||
};
|
||||
|
||||
let responseJson_: any = null;
|
||||
const loadResponseJson = async () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
responseJson_ = JSON.parse(responseText);
|
||||
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
|
||||
return responseJson_;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
if (options.target === 'file') throw newError('fetchBlob error', response.status);
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = await loadResponseJson();
|
||||
} catch (error) {
|
||||
// Just send back the plain text in newErro()
|
||||
try {
|
||||
if (this.debugRequests_) {
|
||||
logger.debug(this.requestToCurl_(url, fetchOptions));
|
||||
}
|
||||
|
||||
if (json && json.error) {
|
||||
throw newError(`${json.error}`, json.code ? json.code : response.status);
|
||||
let response: any = null;
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
if (fetchOptions.path) {
|
||||
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
|
||||
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
|
||||
}
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
throw newError('Unknown error', response.status);
|
||||
const responseText = await response.text();
|
||||
|
||||
if (this.debugRequests_) {
|
||||
logger.debug('Response', responseText);
|
||||
}
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message: string, code: number = 0) => {
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
// return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
return new JoplinError(message, code, `${method} ${path}: ${message} (${code}): ${shortResponseText}`);
|
||||
};
|
||||
|
||||
let responseJson_: any = null;
|
||||
const loadResponseJson = async () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
responseJson_ = JSON.parse(responseText);
|
||||
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
|
||||
return responseJson_;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
if (options.target === 'file') throw newError('fetchBlob error', response.status);
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = await loadResponseJson();
|
||||
} catch (error) {
|
||||
// Just send back the plain text in newErro()
|
||||
}
|
||||
|
||||
if (json && json.error) {
|
||||
throw newError(`${json.error}`, json.code ? json.code : response.status);
|
||||
}
|
||||
|
||||
throw newError('Unknown error', response.status);
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
const output = await loadResponseJson();
|
||||
return output;
|
||||
} catch (error) {
|
||||
if (error.code !== 404) {
|
||||
logger.warn(this.requestToCurl_(url, fetchOptions));
|
||||
logger.warn(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
const output = await loadResponseJson();
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,8 @@
|
||||
const { afterEachCleanUp } = require('./testing/test-utils.js');
|
||||
const { shimInit } = require('./shim-init-node.js');
|
||||
const shim = require('./shim').default;
|
||||
const sharp = require('sharp');
|
||||
|
||||
let keytar;
|
||||
try {
|
||||
keytar = shim.platformSupportsKeyChain() ? require('keytar') : null;
|
||||
} catch (error) {
|
||||
console.error('Cannot load keytar - keychain support will be disabled', error);
|
||||
keytar = null;
|
||||
}
|
||||
|
||||
shimInit(sharp, keytar);
|
||||
shimInit(sharp, null);
|
||||
|
||||
global.afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
|
@@ -7,10 +7,18 @@ const MarkdownIt = require('markdown-it');
|
||||
const listRegex = /^(\s*)([*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]\s))(\s*)/;
|
||||
const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s+)$/;
|
||||
|
||||
export enum MarkdownTableJustify {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Right = 'right,',
|
||||
}
|
||||
|
||||
export interface MarkdownTableHeader {
|
||||
name: string;
|
||||
label: string;
|
||||
filter?: Function;
|
||||
disableEscape?: boolean;
|
||||
justify?: MarkdownTableJustify;
|
||||
}
|
||||
|
||||
export interface MarkdownTableRow {
|
||||
@@ -120,26 +128,38 @@ const markdownUtils = {
|
||||
createMarkdownTable(headers: MarkdownTableHeader[], rows: MarkdownTableRow[]): string {
|
||||
const output = [];
|
||||
|
||||
const minCellWidth = 5;
|
||||
|
||||
const headersMd = [];
|
||||
const lineMd = [];
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const h = headers[i];
|
||||
headersMd.push(stringPadding(h.label, 3, ' ', stringPadding.RIGHT));
|
||||
lineMd.push('---');
|
||||
headersMd.push(stringPadding(h.label, minCellWidth, ' ', stringPadding.RIGHT));
|
||||
|
||||
const justify = h.justify ? h.justify : MarkdownTableJustify.Left;
|
||||
|
||||
if (justify === MarkdownTableJustify.Left) {
|
||||
lineMd.push('-----');
|
||||
} else if (justify === MarkdownTableJustify.Center) {
|
||||
lineMd.push(':---:');
|
||||
} else {
|
||||
lineMd.push('----:');
|
||||
}
|
||||
}
|
||||
|
||||
output.push(headersMd.join(' | '));
|
||||
output.push(lineMd.join(' | '));
|
||||
output.push(`| ${headersMd.join(' | ')} |`);
|
||||
output.push(`| ${lineMd.join(' | ')} |`);
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const rowMd = [];
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
const h = headers[j];
|
||||
const valueMd = markdownUtils.escapeTableCell(h.filter ? h.filter(row[h.name]) : row[h.name]);
|
||||
rowMd.push(stringPadding(valueMd, 3, ' ', stringPadding.RIGHT));
|
||||
const value = (h.filter ? h.filter(row[h.name]) : row[h.name]) || '';
|
||||
const valueMd = h.disableEscape ? value : markdownUtils.escapeTableCell(value);
|
||||
rowMd.push(stringPadding(valueMd, minCellWidth, ' ', stringPadding.RIGHT));
|
||||
}
|
||||
output.push(rowMd.join(' | '));
|
||||
output.push(`| ${rowMd.join(' | ')} |`);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
|
@@ -476,20 +476,6 @@ class Setting extends BaseModel {
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
// 'sync.9.directory': {
|
||||
// value: 'Apps/Joplin',
|
||||
// type: SettingItemType.String,
|
||||
// section: 'sync',
|
||||
// show: (settings: any) => {
|
||||
// return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinServer');
|
||||
// },
|
||||
// filter: value => {
|
||||
// return value ? ltrimSlashes(rtrimSlashes(value)) : '';
|
||||
// },
|
||||
// public: true,
|
||||
// label: () => _('Joplin Server Directory'),
|
||||
// storage: SettingStorage.File,
|
||||
// },
|
||||
'sync.9.username': {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
|
@@ -51,11 +51,20 @@ describe('models_Tag', function() {
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
|
||||
const todo1 = await Note.save({ title: 'todo 1', parent_id: folder1.id, is_todo: 1, todo_completed: 1590085027710 });
|
||||
await Tag.setNoteTagsByTitles(note1.id, ['un']);
|
||||
await Tag.setNoteTagsByTitles(note2.id, ['un']);
|
||||
await Tag.setNoteTagsByTitles(todo1.id, ['un']);
|
||||
|
||||
let tags = await Tag.allWithNotes();
|
||||
expect(tags.length).toBe(1);
|
||||
expect(tags[0].note_count).toBe(3);
|
||||
expect(tags[0].todo_completed_count).toBe(1);
|
||||
|
||||
await Note.delete(todo1.id);
|
||||
|
||||
tags = await Tag.allWithNotes();
|
||||
expect(tags.length).toBe(1);
|
||||
expect(tags[0].note_count).toBe(2);
|
||||
|
||||
await Note.delete(note1.id);
|
||||
@@ -74,6 +83,8 @@ describe('models_Tag', function() {
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
|
||||
const todo1 = await Note.save({ title: 'todo 2', parent_id: folder1.id, is_todo: 1, todo_completed: 1590085027710 });
|
||||
const todo2 = await Note.save({ title: 'todo 2', parent_id: folder1.id, is_todo: 1 });
|
||||
const tag = await Tag.save({ title: 'mytag' });
|
||||
await Tag.addNote(tag.id, note1.id);
|
||||
|
||||
@@ -83,6 +94,12 @@ describe('models_Tag', function() {
|
||||
await Tag.addNote(tag.id, note2.id);
|
||||
tagWithCount = await Tag.loadWithCount(tag.id);
|
||||
expect(tagWithCount.note_count).toBe(2);
|
||||
|
||||
await Tag.addNote(tag.id, todo1.id);
|
||||
await Tag.addNote(tag.id, todo2.id);
|
||||
tagWithCount = await Tag.loadWithCount(tag.id);
|
||||
expect(tagWithCount.note_count).toBe(4);
|
||||
expect(tagWithCount.todo_completed_count).toBe(1);
|
||||
}));
|
||||
|
||||
it('should get common tags for set of notes', (async () => {
|
||||
@@ -131,6 +148,7 @@ describe('models_Tag', function() {
|
||||
expect(commonTagIds.includes(tagb.id)).toBe(true);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([note3.id]);
|
||||
|
||||
commonTagIds = commonTags.map(t => t.id);
|
||||
expect(commonTags.length).toBe(3);
|
||||
expect(commonTagIds.includes(taga.id)).toBe(true);
|
||||
|
20
packages/lib/package-lock.json
generated
20
packages/lib/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -4059,24 +4059,6 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"keytar": {
|
||||
"version": "7.7.0",
|
||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz",
|
||||
"integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-addon-api": "^3.0.0",
|
||||
"prebuild-install": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.0.tgz",
|
||||
"integrity": "sha512-kcwSAWhPi4+QzAtsL2+2s/awvDo2GKLsvMCwNRxb5BUshteXU8U97NCyvQDsGKs/m0He9WcG4YWew/BnuLx++w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -16,21 +16,20 @@
|
||||
"test-ci": "npm run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"clean-html": "^1.5.0",
|
||||
"jest": "^26.6.3",
|
||||
"sharp": "^0.26.2",
|
||||
"keytar": "^7.0.0",
|
||||
"typescript": "^4.0.5",
|
||||
"clean-html": "^1.5.0"
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.24",
|
||||
"@joplin/fork-sax": "^1.2.28",
|
||||
"@joplin/fork-htmlparser2": "^4.1.26",
|
||||
"@joplin/fork-sax": "^1.2.30",
|
||||
"@joplin/renderer": "^1.8.2",
|
||||
"@joplin/turndown": "^4.0.46",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.28",
|
||||
"@joplin/turndown": "^4.0.48",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.30",
|
||||
"async-mutex": "^0.1.3",
|
||||
"aws-sdk": "^2.588.0",
|
||||
"base-64": "^0.1.0",
|
||||
|
@@ -1,8 +1,21 @@
|
||||
import Logger from '../../Logger';
|
||||
import shim from '../../shim';
|
||||
import { PluginManifest } from './utils/types';
|
||||
const md5 = require('md5');
|
||||
const compareVersions = require('compare-versions');
|
||||
|
||||
const logger = Logger.create('RepositoryApi');
|
||||
|
||||
interface ReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
upload_url: string;
|
||||
assets: ReleaseAsset[];
|
||||
}
|
||||
|
||||
export default class RepositoryApi {
|
||||
|
||||
// As a base URL, this class can support either a remote repository or a
|
||||
@@ -14,6 +27,7 @@ export default class RepositoryApi {
|
||||
// Later on, other repo types could be supported.
|
||||
private baseUrl_: string;
|
||||
private tempDir_: string;
|
||||
private release_: Release = null;
|
||||
private manifests_: PluginManifest[] = null;
|
||||
|
||||
public constructor(baseUrl: string, tempDir: string) {
|
||||
@@ -21,7 +35,12 @@ export default class RepositoryApi {
|
||||
this.tempDir_ = tempDir;
|
||||
}
|
||||
|
||||
public async loadManifests() {
|
||||
public async initialize() {
|
||||
await this.loadManifests();
|
||||
await this.loadRelease();
|
||||
}
|
||||
|
||||
private async loadManifests() {
|
||||
const manifestsText = await this.fetchText('manifests.json');
|
||||
try {
|
||||
const manifests = JSON.parse(manifestsText);
|
||||
@@ -34,6 +53,27 @@ export default class RepositoryApi {
|
||||
}
|
||||
}
|
||||
|
||||
private get githubApiUrl(): string {
|
||||
// https://github.com/joplin/plugins
|
||||
// https://api.github.com/repos/joplin/plugins/releases
|
||||
return this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3');
|
||||
}
|
||||
|
||||
private async loadRelease() {
|
||||
this.release_ = null;
|
||||
|
||||
if (this.isLocalRepo) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.githubApiUrl}/releases`);
|
||||
const releases = await response.json();
|
||||
if (!releases.length) throw new Error('No release was found');
|
||||
this.release_ = releases[0];
|
||||
} catch (error) {
|
||||
logger.warn('Could not load release - files will be downloaded from the repository directly:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private get isLocalRepo(): boolean {
|
||||
return this.baseUrl_.indexOf('http') !== 0;
|
||||
}
|
||||
@@ -46,15 +86,34 @@ export default class RepositoryApi {
|
||||
}
|
||||
}
|
||||
|
||||
private fileUrl(relativePath: string): string {
|
||||
private assetFileUrl(pluginId: string): string {
|
||||
if (this.release_) {
|
||||
const asset = this.release_.assets.find(asset => {
|
||||
const s = asset.name.split('@');
|
||||
s.pop();
|
||||
const id = s.join('@');
|
||||
return id === pluginId;
|
||||
});
|
||||
|
||||
if (asset) return asset.browser_download_url;
|
||||
|
||||
logger.warn(`Could not get plugin from release: ${pluginId}`);
|
||||
}
|
||||
|
||||
// If we couldn't get the plugin file from the release, get it directly
|
||||
// from the repository instead.
|
||||
return this.repoFileUrl(`plugins/${pluginId}/plugin.jpl`);
|
||||
}
|
||||
|
||||
private repoFileUrl(relativePath: string): string {
|
||||
return `${this.contentBaseUrl}/${relativePath}`;
|
||||
}
|
||||
|
||||
private async fetchText(path: string): Promise<string> {
|
||||
if (this.isLocalRepo) {
|
||||
return shim.fsDriver().readFile(this.fileUrl(path), 'utf8');
|
||||
return shim.fsDriver().readFile(this.repoFileUrl(path), 'utf8');
|
||||
} else {
|
||||
return shim.fetchText(this.fileUrl(path));
|
||||
return shim.fetchText(this.repoFileUrl(path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +145,7 @@ export default class RepositoryApi {
|
||||
const manifest = manifests.find(m => m.id === pluginId);
|
||||
if (!manifest) throw new Error(`No manifest for plugin ID "${pluginId}"`);
|
||||
|
||||
const fileUrl = this.fileUrl(`plugins/${manifest.id}/plugin.jpl`);
|
||||
const fileUrl = this.assetFileUrl(manifest.id); // this.repoFileUrl(`plugins/${manifest.id}/plugin.jpl`);
|
||||
const hash = md5(Date.now() + Math.random());
|
||||
const targetPath = `${this.tempDir_}/${hash}_${manifest.id}.jpl`;
|
||||
|
||||
|
@@ -48,10 +48,8 @@ export interface Command {
|
||||
* Currently the supported context variables aren't documented, but you can
|
||||
* find the list below:
|
||||
*
|
||||
* - [Global When
|
||||
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts).
|
||||
* - [Desktop app When
|
||||
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts).
|
||||
* - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts)
|
||||
* - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts)
|
||||
*
|
||||
* Note: Commands are enabled by default unless you use this property.
|
||||
*/
|
||||
|
@@ -13,8 +13,6 @@ import KeymapService from '../services/KeymapService';
|
||||
import KvStore from '../services/KvStore';
|
||||
import KeychainServiceDriver from '../services/keychain/KeychainServiceDriver.node';
|
||||
import KeychainServiceDriverDummy from '../services/keychain/KeychainServiceDriver.dummy';
|
||||
import PluginRunner from '../../app-cli/app/services/plugins/PluginRunner';
|
||||
import PluginService from '../services/plugins/PluginService';
|
||||
import FileApiDriverJoplinServer from '../file-api-driver-joplinServer';
|
||||
import OneDriveApi from '../onedrive-api';
|
||||
import SyncTargetOneDrive from '../SyncTargetOneDrive';
|
||||
@@ -766,45 +764,6 @@ async function createTempDir() {
|
||||
return tempDirPath;
|
||||
}
|
||||
|
||||
interface PluginServiceOptions {
|
||||
getState?(): Record<string, any>;
|
||||
}
|
||||
|
||||
function newPluginService(appVersion = '1.4', options: PluginServiceOptions = null): PluginService {
|
||||
options = options || {};
|
||||
|
||||
const runner = new PluginRunner();
|
||||
const service = new PluginService();
|
||||
service.initialize(
|
||||
appVersion,
|
||||
{
|
||||
joplin: {},
|
||||
},
|
||||
runner,
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: options.getState ? options.getState : () => {},
|
||||
}
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
function newPluginScript(script: string) {
|
||||
return `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.PluginTest",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
|
||||
${script}
|
||||
`;
|
||||
}
|
||||
|
||||
async function waitForFolderCount(count: number) {
|
||||
const timeout = 2000;
|
||||
const startTime = Date.now();
|
||||
@@ -901,4 +860,4 @@ class TestApp extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
export { supportDir, waitForFolderCount, afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
export { supportDir, waitForFolderCount, afterAllCleanUp, exportDir, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
|
@@ -1,49 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.readCredentialFile = exports.credentialFile = exports.credentialDir = void 0;
|
||||
const fs = require('fs-extra');
|
||||
function credentialDir() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const username = require('os').userInfo().username;
|
||||
const toTry = [
|
||||
`c:/Users/${username}/joplin-credentials`,
|
||||
`/mnt/c/Users/${username}/joplin-credentials`,
|
||||
`/home/${username}/joplin-credentials`,
|
||||
`/Users/${username}/joplin-credentials`,
|
||||
];
|
||||
for (const dirPath of toTry) {
|
||||
if (yield fs.pathExists(dirPath))
|
||||
return dirPath;
|
||||
}
|
||||
throw new Error(`Could not find credential directory in any of these paths: ${JSON.stringify(toTry)}`);
|
||||
});
|
||||
}
|
||||
exports.credentialDir = credentialDir;
|
||||
function credentialFile(filename) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const rootDir = yield credentialDir();
|
||||
const output = `${rootDir}/${filename}`;
|
||||
if (!(yield fs.pathExists(output)))
|
||||
throw new Error(`No such file: ${output}`);
|
||||
return output;
|
||||
});
|
||||
}
|
||||
exports.credentialFile = credentialFile;
|
||||
function readCredentialFile(filename) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const filePath = yield credentialFile(filename);
|
||||
const r = yield fs.readFile(filePath);
|
||||
return r.toString();
|
||||
});
|
||||
}
|
||||
exports.readCredentialFile = readCredentialFile;
|
||||
//# sourceMappingURL=credentialFiles.js.map
|
@@ -24,8 +24,12 @@ export async function credentialFile(filename: string) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function readCredentialFile(filename: string) {
|
||||
const filePath = await credentialFile(filename);
|
||||
const r = await fs.readFile(filePath);
|
||||
return r.toString();
|
||||
export async function readCredentialFile(filename: string, defaultValue: string = '') {
|
||||
try {
|
||||
const filePath = await credentialFile(filename);
|
||||
const r = await fs.readFile(filePath);
|
||||
return r.toString();
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
171
packages/plugin-repo-cli/commands/updateRelease.ts
Normal file
171
packages/plugin-repo-cli/commands/updateRelease.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { githubOauthToken } from '@joplin/tools/tool-utils';
|
||||
import { pathExists, readdir, readFile, stat, writeFile } from 'fs-extra';
|
||||
const ghReleaseAssets = require('gh-release-assets');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const apiBaseUrl = 'https://api.github.com/repos/joplin/plugins';
|
||||
|
||||
interface Args {
|
||||
pluginRepoDir: string;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
interface PluginInfo {
|
||||
id: string;
|
||||
version: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface ReleaseAsset {
|
||||
id: number;
|
||||
name: string;
|
||||
download_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
upload_url: string;
|
||||
assets: ReleaseAsset[];
|
||||
}
|
||||
|
||||
async function getRelease(): Promise<Release> {
|
||||
const response = await fetch(`${apiBaseUrl}/releases`);
|
||||
const releases = await response.json();
|
||||
if (!releases.length) throw new Error('No existing release');
|
||||
return releases[0];
|
||||
}
|
||||
|
||||
async function getPluginInfos(pluginRepoDir: string): Promise<PluginInfo[]> {
|
||||
const pluginDirs = await readdir(`${pluginRepoDir}/plugins`);
|
||||
const output: PluginInfo[] = [];
|
||||
|
||||
for (const pluginDir of pluginDirs) {
|
||||
const basePath = `${pluginRepoDir}/plugins/${pluginDir}`;
|
||||
const manifest = JSON.parse(await readFile(`${basePath}/manifest.json`, 'utf8'));
|
||||
output.push({
|
||||
id: manifest.id,
|
||||
version: manifest.version,
|
||||
path: `${basePath}/plugin.jpl`,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function assetNameFromPluginInfo(pluginInfo: PluginInfo): string {
|
||||
return `${pluginInfo.id}@${pluginInfo.version}.jpl`;
|
||||
}
|
||||
|
||||
function pluginInfoFromAssetName(name: string): PluginInfo {
|
||||
let s = name.split('.');
|
||||
s.pop();
|
||||
s = s.join('.').split('@');
|
||||
return {
|
||||
id: s[0],
|
||||
version: s[1],
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteAsset(oauthToken: string, id: number) {
|
||||
await fetch(`${apiBaseUrl}/releases/assets/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${oauthToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAsset(oauthToken: string, uploadUrl: string, pluginInfo: PluginInfo) {
|
||||
return new Promise((resolve: Function, reject: Function) => {
|
||||
ghReleaseAssets({
|
||||
url: uploadUrl,
|
||||
token: oauthToken,
|
||||
assets: [
|
||||
{
|
||||
name: assetNameFromPluginInfo(pluginInfo),
|
||||
path: pluginInfo.path,
|
||||
},
|
||||
],
|
||||
}, (error: Error, assets: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(assets);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createStats(statFilePath: string, release: Release) {
|
||||
const output: Record<string, any> = await pathExists(statFilePath) ? JSON.parse(await readFile(statFilePath, 'utf8')) : {};
|
||||
|
||||
if (release.assets) {
|
||||
for (const asset of release.assets) {
|
||||
const pluginInfo = pluginInfoFromAssetName(asset.name);
|
||||
if (!output[pluginInfo.id]) output[pluginInfo.id] = {};
|
||||
|
||||
output[pluginInfo.id][pluginInfo.version] = {
|
||||
downloadCount: asset.download_count,
|
||||
createdAt: asset.created_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function saveStats(statFilePath: string, stats: any) {
|
||||
await writeFile(statFilePath, JSON.stringify(stats, null, '\t'));
|
||||
}
|
||||
|
||||
export default async function(args: Args) {
|
||||
const release = await getRelease();
|
||||
const statFilePath = `${args.pluginRepoDir}/stats.json`;
|
||||
const stats = await createStats(statFilePath, release);
|
||||
|
||||
// We save the stats:
|
||||
// - If the stat file doesn't exist
|
||||
// - Or every x hours
|
||||
// - Or before deleting an asset (so that we preserve the number of times a
|
||||
// particular version of a plugin has been downloaded).
|
||||
let statSaved = false;
|
||||
async function doSaveStats() {
|
||||
if (statSaved) return;
|
||||
console.info('Updating stats file...');
|
||||
await saveStats(statFilePath, stats);
|
||||
statSaved = true;
|
||||
}
|
||||
|
||||
if (!(await pathExists(statFilePath))) {
|
||||
await doSaveStats();
|
||||
} else {
|
||||
const fileInfo = await stat(statFilePath);
|
||||
if (Date.now() - fileInfo.mtime.getTime() >= 24 * 60 * 60 * 1000) {
|
||||
await doSaveStats();
|
||||
}
|
||||
}
|
||||
|
||||
const pluginInfos = await getPluginInfos(args.pluginRepoDir);
|
||||
const oauthToken = await githubOauthToken();
|
||||
|
||||
for (const pluginInfo of pluginInfos) {
|
||||
const assetName = assetNameFromPluginInfo(pluginInfo);
|
||||
|
||||
const otherVersionAssets = release.assets.filter(asset => {
|
||||
const info = pluginInfoFromAssetName(asset.name);
|
||||
return info.id === pluginInfo.id && info.version !== pluginInfo.version;
|
||||
});
|
||||
|
||||
for (const asset of otherVersionAssets) {
|
||||
console.info(`Deleting old asset ${asset.name}...`);
|
||||
await doSaveStats();
|
||||
await deleteAsset(oauthToken, asset.id);
|
||||
}
|
||||
|
||||
const existingAsset = release.assets.find(asset => asset.name === assetName);
|
||||
if (existingAsset) continue;
|
||||
console.info(`Uploading ${assetName}...`);
|
||||
await uploadAsset(oauthToken, release.upload_url, pluginInfo);
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ import checkIfPluginCanBeAdded from './lib/checkIfPluginCanBeAdded';
|
||||
import updateReadme from './lib/updateReadme';
|
||||
import { NpmPackage } from './lib/types';
|
||||
import gitCompareUrl from './lib/gitCompareUrl';
|
||||
import commandUpdateRelease from './commands/updateRelease';
|
||||
|
||||
function stripOffPackageOrg(name: string): string {
|
||||
const n = name.split('/');
|
||||
@@ -249,6 +250,14 @@ async function commandBuild(args: CommandBuildArgs) {
|
||||
await processNpmPackage(npmPackage, repoDir);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await commandUpdateRelease(args);
|
||||
if (!(await gitRepoClean())) {
|
||||
await execCommand2('git add -A', { showOutput: true });
|
||||
await execCommand2('git commit -m "Update stats"', { showOutput: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!dryRun) await execCommand2('git push');
|
||||
}
|
||||
|
||||
@@ -263,6 +272,7 @@ async function main() {
|
||||
const commands: Record<string, Function> = {
|
||||
build: commandBuild,
|
||||
version: commandVersion,
|
||||
updateRelease: commandUpdateRelease,
|
||||
};
|
||||
|
||||
let selectedCommand: string = '';
|
||||
@@ -286,6 +296,8 @@ async function main() {
|
||||
|
||||
.command('version', 'Gives version info', () => {}, (args: any) => setSelectedCommand('version', args))
|
||||
|
||||
.command('update-release <plugin-repo-dir>', 'Update GitHub release', () => {}, (args: any) => setSelectedCommand('updateRelease', args))
|
||||
|
||||
.help()
|
||||
.argv;
|
||||
|
||||
|
179
packages/plugin-repo-cli/package-lock.json
generated
179
packages/plugin-repo-cli/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -907,6 +907,11 @@
|
||||
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
|
||||
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1296,8 +1301,7 @@
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
@@ -1382,6 +1386,14 @@
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"dev": true
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"requires": {
|
||||
"mimic-response": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||
@@ -1470,6 +1482,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz",
|
||||
"integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -1495,7 +1530,6 @@
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
@@ -1889,6 +1923,19 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"gh-release-assets": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gh-release-assets/-/gh-release-assets-2.0.0.tgz",
|
||||
"integrity": "sha512-I+Gy+e86o7A6J7sJRX4uA3EvLlLFcXxsRre22YTJ5dzpl/elZA75bMWfoBd0WVY3Mp9M8KtROfn3zlzDkptyWw==",
|
||||
"requires": {
|
||||
"async": "^3.2.0",
|
||||
"mime": "^2.4.6",
|
||||
"progress-stream": "^2.0.0",
|
||||
"pumpify": "^2.0.1",
|
||||
"simple-get": "^4.0.0",
|
||||
"util-extend": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
@@ -2080,8 +2127,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ip-regex": {
|
||||
"version": "2.1.0",
|
||||
@@ -2254,8 +2300,7 @@
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
@@ -3225,6 +3270,11 @@
|
||||
"picomatch": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
|
||||
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
@@ -3246,6 +3296,11 @@
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -3319,6 +3374,11 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -3460,7 +3520,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -3626,6 +3685,20 @@
|
||||
"react-is": "^17.0.1"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"progress-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz",
|
||||
"integrity": "sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=",
|
||||
"requires": {
|
||||
"speedometer": "~1.0.0",
|
||||
"through2": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
|
||||
@@ -3646,12 +3719,21 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"pumpify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
|
||||
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
|
||||
"requires": {
|
||||
"duplexify": "^4.1.1",
|
||||
"inherits": "^2.0.3",
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -3701,6 +3783,20 @@
|
||||
"type-fest": "^0.8.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"regex-not": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
|
||||
@@ -3873,8 +3969,7 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
@@ -4104,6 +4199,21 @@
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
},
|
||||
"simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz",
|
||||
"integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==",
|
||||
"requires": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -4311,6 +4421,11 @@
|
||||
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"speedometer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz",
|
||||
"integrity": "sha1-zWccsGdSwivKM3Di8zREC+T8YuI="
|
||||
},
|
||||
"split-string": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
@@ -4387,6 +4502,11 @@
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
|
||||
"dev": true
|
||||
},
|
||||
"stream-shift": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
||||
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
|
||||
},
|
||||
"string-length": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz",
|
||||
@@ -4407,6 +4527,14 @@
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
@@ -4485,6 +4613,15 @@
|
||||
"integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
|
||||
"dev": true
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
|
||||
"requires": {
|
||||
"readable-stream": "~2.3.6",
|
||||
"xtend": "~4.0.1"
|
||||
}
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
||||
@@ -4687,6 +4824,16 @@
|
||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"util-extend": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
|
||||
"integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
@@ -4827,8 +4974,7 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
@@ -4860,6 +5006,11 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
@@ -21,6 +21,8 @@
|
||||
"@joplin/lib": "^1.8.2",
|
||||
"@joplin/tools": "^1.8.2",
|
||||
"fs-extra": "^9.0.1",
|
||||
"gh-release-assets": "^2.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"yargs": "^16.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
2
packages/renderer/package-lock.json
generated
2
packages/renderer/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "The Joplin note renderer, used the mobile and desktop application",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
|
||||
"main": "index.js",
|
||||
@@ -24,7 +24,7 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.24",
|
||||
"@joplin/fork-htmlparser2": "^4.1.26",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^10.2.1",
|
||||
|
3
packages/server/.gitignore
vendored
3
packages/server/.gitignore
vendored
@@ -6,4 +6,5 @@ db-*.sqlite
|
||||
*.pid
|
||||
logs/
|
||||
tests/temp/
|
||||
temp/
|
||||
temp/
|
||||
.env
|
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"watch": ["dist/", "../renderer", "../lib"]
|
||||
"watch": [
|
||||
"dist/",
|
||||
"../renderer",
|
||||
"../lib",
|
||||
"src/views"
|
||||
]
|
||||
}
|
37
packages/server/package-lock.json
generated
37
packages/server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1389,6 +1389,15 @@
|
||||
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/nodemailer": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.1.tgz",
|
||||
"integrity": "sha512-8081UY/0XTTDpuGqCnDc8IY+Q3DSg604wB3dBH0CaZlj4nZWHWuxtZ3NRZ9c9WUrz1Vfm6wioAUnqL3bsh49uQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||
@@ -5958,6 +5967,19 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.33",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz",
|
||||
"integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==",
|
||||
"requires": {
|
||||
"moment": ">= 2.9.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -6045,6 +6067,14 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-cron": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz",
|
||||
"integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==",
|
||||
"requires": {
|
||||
"moment-timezone": "^0.5.31"
|
||||
}
|
||||
},
|
||||
"node-env-file": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/node-env-file/-/node-env-file-0.1.8.tgz",
|
||||
@@ -6148,6 +6178,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.0.tgz",
|
||||
"integrity": "sha512-ikSMDU1nZqpo2WUPE0wTTw/NGGImTkwpJKDIFPZT+YvvR9Sj+ze5wzu95JHkBMglQLoG2ITxU21WukCC/XsFkg=="
|
||||
},
|
||||
"nodemon": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz",
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
"start": "node dist/app.js",
|
||||
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
@@ -30,11 +30,13 @@
|
||||
"mustache": "^3.1.0",
|
||||
"nanoid": "^2.1.1",
|
||||
"node-env-file": "^0.1.8",
|
||||
"nodemailer": "^6.6.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"pg": "^8.5.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"query-string": "^6.8.3",
|
||||
"sqlite3": "^4.1.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"yargs": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -46,6 +48,7 @@
|
||||
"@types/koa": "^2.0.49",
|
||||
"@types/markdown-it": "^12.0.0",
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/nodemailer": "^6.4.1",
|
||||
"@types/yargs": "^13.0.2",
|
||||
"jest": "^26.6.3",
|
||||
"jsdom": "^16.4.0",
|
||||
|
@@ -10,10 +10,6 @@ input.form-control {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 1rem 3rem;
|
||||
}
|
||||
|
||||
.navbar .logo-container {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -34,7 +30,7 @@ input.form-control {
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 3rem;
|
||||
padding: 2rem 3rem;
|
||||
}
|
||||
|
||||
table.table .nowrap {
|
||||
@@ -47,4 +43,14 @@ table.table .stretch {
|
||||
|
||||
table.table th .sort-button i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.footer .container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
opacity: 0.5;
|
||||
}
|
Binary file not shown.
@@ -7,7 +7,7 @@ import { argv } from 'yargs';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, runningInDocker, EnvVariables } from './config';
|
||||
import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteFilePath } from './db';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
|
||||
import { AppContext, Env } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import routeHandler from './middleware/routeHandler';
|
||||
@@ -16,7 +16,6 @@ import ownerHandler from './middleware/ownerHandler';
|
||||
import setupAppContext from './utils/setupAppContext';
|
||||
import { initializeJoplinUtils } from './utils/joplinUtils';
|
||||
import startServices from './utils/startServices';
|
||||
// import { createItemTree } from './utils/testing/testUtils';
|
||||
|
||||
const nodeEnvFile = require('node-env-file');
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
@@ -26,12 +25,14 @@ const env: Env = argv.env as Env || Env.Prod;
|
||||
|
||||
const envVariables: Record<Env, EnvVariables> = {
|
||||
dev: {
|
||||
SQLITE_DATABASE: 'dev',
|
||||
SQLITE_DATABASE: `${sqliteDefaultDir}/db-dev.sqlite`,
|
||||
},
|
||||
buildTypes: {
|
||||
SQLITE_DATABASE: 'buildTypes',
|
||||
SQLITE_DATABASE: `${sqliteDefaultDir}/db-buildTypes.sqlite`,
|
||||
},
|
||||
prod: {
|
||||
SQLITE_DATABASE: `${sqliteDefaultDir}/db-prod.sqlite`,
|
||||
},
|
||||
prod: {}, // Actually get the env variables from the environment
|
||||
};
|
||||
|
||||
let appLogger_: LoggerWrapper = null;
|
||||
@@ -67,14 +68,25 @@ function markPasswords(o: Record<string, any>): Record<string, any> {
|
||||
return output;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (argv.envFile) {
|
||||
nodeEnvFile(argv.envFile);
|
||||
async function getEnvFilePath(env: Env, argv: any): Promise<string> {
|
||||
if (argv.envFile) return argv.envFile;
|
||||
|
||||
if (env === Env.Dev) {
|
||||
const envFilePath = `${require('os').homedir()}/joplin-credentials/server.env`;
|
||||
if (await fs.pathExists(envFilePath)) return envFilePath;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const envFilePath = await getEnvFilePath(env, argv);
|
||||
|
||||
if (envFilePath) nodeEnvFile(envFilePath);
|
||||
|
||||
if (!envVariables[env]) throw new Error(`Invalid env: ${env}`);
|
||||
|
||||
initConfig({
|
||||
await initConfig({
|
||||
...envVariables[env],
|
||||
...process.env,
|
||||
});
|
||||
@@ -91,6 +103,8 @@ async function main() {
|
||||
});
|
||||
Logger.initializeGlobalLogger(globalLogger);
|
||||
|
||||
if (envFilePath) appLogger().info(`Env variables were loaded from: ${envFilePath}`);
|
||||
|
||||
const pidFile = argv.pidfile as string;
|
||||
|
||||
if (pidFile) {
|
||||
@@ -112,12 +126,13 @@ async function main() {
|
||||
} else if (argv.createDb) {
|
||||
await createDb(config().database);
|
||||
} else {
|
||||
appLogger().info(`Starting server (${env}) on port ${config().port} and PID ${process.pid}...`);
|
||||
appLogger().info(`Starting server v${config().appVersion} (${env}) on port ${config().port} and PID ${process.pid}...`);
|
||||
appLogger().info('Running in Docker:', runningInDocker());
|
||||
appLogger().info('Public base URL:', config().baseUrl);
|
||||
appLogger().info('API base URL:', config().apiBaseUrl);
|
||||
appLogger().info('User content base URL:', config().userContentBaseUrl);
|
||||
appLogger().info('Log dir:', config().logDir);
|
||||
appLogger().info('DB Config:', markPasswords(config().database));
|
||||
if (config().database.client === 'sqlite3') appLogger().info('DB file:', sqliteFilePath(config().database.name));
|
||||
|
||||
appLogger().info('Trying to connect to database...');
|
||||
const connectionCheck = await waitForConnection(config().database);
|
||||
@@ -129,7 +144,7 @@ async function main() {
|
||||
const appContext = app.context as AppContext;
|
||||
|
||||
await setupAppContext(appContext, env, connectionCheck.connection, appLogger);
|
||||
await initializeJoplinUtils(config(), appContext.models);
|
||||
await initializeJoplinUtils(config(), appContext.models, appContext.services.mustache);
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(appContext.db);
|
||||
@@ -137,37 +152,7 @@ async function main() {
|
||||
appLogger().info('Starting services...');
|
||||
await startServices(appContext);
|
||||
|
||||
// if (env !== Env.Prod) {
|
||||
// const done = await handleDebugCommands(argv, appContext.db, config());
|
||||
// if (done) {
|
||||
// appLogger().info('Debug command has been executed. Now starting server...');
|
||||
// }
|
||||
// }
|
||||
|
||||
appLogger().info(`Call this for testing: \`curl ${config().baseUrl}/api/ping\``);
|
||||
|
||||
// const tree: any = {
|
||||
// '000000000000000000000000000000F1': {},
|
||||
// '000000000000000000000000000000F2': {
|
||||
// '00000000000000000000000000000001': null,
|
||||
// '00000000000000000000000000000002': null,
|
||||
// },
|
||||
// '000000000000000000000000000000F3': {
|
||||
// '00000000000000000000000000000003': null,
|
||||
// '000000000000000000000000000000F4': {
|
||||
// '00000000000000000000000000000004': null,
|
||||
// '00000000000000000000000000000005': null,
|
||||
// },
|
||||
// },
|
||||
// '00000000000000000000000000000006': null,
|
||||
// '00000000000000000000000000000007': null,
|
||||
// };
|
||||
|
||||
// const users = await appContext.models.user().all();
|
||||
|
||||
// const itemModel = appContext.models.item({ userId: users[0].id });
|
||||
|
||||
// await createItemTree(itemModel, '', tree);
|
||||
appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``);
|
||||
|
||||
app.listen(config().port);
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
import { rtrimSlashes } from '@joplin/lib/path-utils';
|
||||
import { Config, DatabaseConfig, DatabaseConfigClient } from './utils/types';
|
||||
import { Config, DatabaseConfig, DatabaseConfigClient, MailerConfig, RouteType } from './utils/types';
|
||||
import * as pathUtils from 'path';
|
||||
import { readFile } from 'fs-extra';
|
||||
|
||||
export interface EnvVariables {
|
||||
APP_BASE_URL?: string;
|
||||
USER_CONTENT_BASE_URL?: string;
|
||||
API_BASE_URL?: string;
|
||||
|
||||
APP_PORT?: string;
|
||||
DB_CLIENT?: string;
|
||||
RUNNING_IN_DOCKER?: string;
|
||||
@@ -14,6 +18,16 @@ export interface EnvVariables {
|
||||
POSTGRES_HOST?: string;
|
||||
POSTGRES_PORT?: string;
|
||||
|
||||
MAILER_ENABLED?: string;
|
||||
MAILER_HOST?: string;
|
||||
MAILER_PORT?: string;
|
||||
MAILER_SECURE?: string;
|
||||
MAILER_AUTH_USER?: string;
|
||||
MAILER_AUTH_PASSWORD?: string;
|
||||
MAILER_NOREPLY_NAME?: string;
|
||||
MAILER_NOREPLY_EMAIL?: string;
|
||||
|
||||
// This must be the full path to the database file
|
||||
SQLITE_DATABASE?: string;
|
||||
}
|
||||
|
||||
@@ -52,11 +66,24 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
|
||||
|
||||
return {
|
||||
client: DatabaseConfigClient.SQLite,
|
||||
name: env.SQLITE_DATABASE || 'prod',
|
||||
name: env.SQLITE_DATABASE,
|
||||
asyncStackTraces: true,
|
||||
};
|
||||
}
|
||||
|
||||
function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
|
||||
return {
|
||||
enabled: env.MAILER_ENABLED !== '0',
|
||||
host: env.MAILER_HOST || '',
|
||||
port: Number(env.MAILER_PORT || 587),
|
||||
secure: !!Number(env.MAILER_SECURE) || true,
|
||||
authUser: env.MAILER_AUTH_USER || '',
|
||||
authPassword: env.MAILER_AUTH_PASSWORD || '',
|
||||
noReplyName: env.MAILER_NOREPLY_NAME || '',
|
||||
noReplyEmail: env.MAILER_NOREPLY_EMAIL || '',
|
||||
};
|
||||
}
|
||||
|
||||
function baseUrlFromEnv(env: any, appPort: number): string {
|
||||
if (env.APP_BASE_URL) {
|
||||
return rtrimSlashes(env.APP_BASE_URL);
|
||||
@@ -65,28 +92,60 @@ function baseUrlFromEnv(env: any, appPort: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
interface PackageJson {
|
||||
version: string;
|
||||
}
|
||||
|
||||
async function readPackageJson(filePath: string): Promise<PackageJson> {
|
||||
const text = await readFile(filePath, 'utf8');
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
let config_: Config = null;
|
||||
|
||||
export function initConfig(env: EnvVariables, overrides: any = null) {
|
||||
export async function initConfig(env: EnvVariables, overrides: any = null) {
|
||||
runningInDocker_ = !!env.RUNNING_IN_DOCKER;
|
||||
|
||||
const rootDir = pathUtils.dirname(__dirname);
|
||||
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
|
||||
|
||||
const packageJson = await readPackageJson(`${rootDir}/package.json`);
|
||||
|
||||
const viewDir = `${rootDir}/src/views`;
|
||||
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
|
||||
const baseUrl = baseUrlFromEnv(env, appPort);
|
||||
|
||||
config_ = {
|
||||
appVersion: packageJson.version,
|
||||
appName: 'Joplin Server',
|
||||
rootDir: rootDir,
|
||||
viewDir: viewDir,
|
||||
layoutDir: `${viewDir}/layouts`,
|
||||
tempDir: `${rootDir}/temp`,
|
||||
logDir: `${rootDir}/logs`,
|
||||
database: databaseConfigFromEnv(runningInDocker_, env),
|
||||
mailer: mailerConfigFromEnv(env),
|
||||
port: appPort,
|
||||
baseUrl: baseUrlFromEnv(env, appPort),
|
||||
baseUrl,
|
||||
apiBaseUrl: env.API_BASE_URL ? env.API_BASE_URL : baseUrl,
|
||||
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function baseUrl(type: RouteType): string {
|
||||
if (type === RouteType.Web) return config().baseUrl;
|
||||
if (type === RouteType.Api) return config().apiBaseUrl;
|
||||
if (type === RouteType.UserContent) return config().userContentBaseUrl;
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
// User content URL is not supported for now so only show the URL if the
|
||||
// user content is hosted on the same domain. Needs to get cookie working
|
||||
// across domains to get user content url working.
|
||||
export function showItemUrls(config: Config): boolean {
|
||||
return config.userContentBaseUrl === config.baseUrl;
|
||||
}
|
||||
|
||||
function config(): Config {
|
||||
if (!config_) throw new Error('Config has not been initialized!');
|
||||
return config_;
|
||||
|
@@ -17,7 +17,7 @@ require('pg').types.setTypeParser(20, function(val: any) {
|
||||
const logger = Logger.create('db');
|
||||
|
||||
const migrationDir = `${__dirname}/migrations`;
|
||||
const sqliteDbDir = pathUtils.dirname(__dirname);
|
||||
export const sqliteDefaultDir = pathUtils.dirname(__dirname);
|
||||
|
||||
export const defaultAdminEmail = 'admin@localhost';
|
||||
export const defaultAdminPassword = 'admin';
|
||||
@@ -47,15 +47,11 @@ export interface ConnectionCheckResult {
|
||||
connection: DbConnection;
|
||||
}
|
||||
|
||||
export function sqliteFilePath(name: string): string {
|
||||
return `${sqliteDbDir}/db-${name}.sqlite`;
|
||||
}
|
||||
|
||||
export function makeKnexConfig(dbConfig: DatabaseConfig): KnexDatabaseConfig {
|
||||
const connection: DbConfigConnection = {};
|
||||
|
||||
if (dbConfig.client === 'sqlite3') {
|
||||
connection.filename = sqliteFilePath(dbConfig.name);
|
||||
connection.filename = dbConfig.name;
|
||||
} else {
|
||||
connection.database = dbConfig.name;
|
||||
connection.host = dbConfig.host;
|
||||
@@ -218,6 +214,11 @@ export enum ItemType {
|
||||
User,
|
||||
}
|
||||
|
||||
export enum EmailSender {
|
||||
NoReply = 1,
|
||||
Support = 2,
|
||||
}
|
||||
|
||||
export enum ChangeType {
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
@@ -277,6 +278,10 @@ export interface User extends WithDates, WithUuid {
|
||||
is_admin?: number;
|
||||
max_item_size?: number;
|
||||
can_share?: number;
|
||||
email_confirmed?: number;
|
||||
must_set_password?: number;
|
||||
account_type?: number;
|
||||
can_upload?: number;
|
||||
}
|
||||
|
||||
export interface Session extends WithDates, WithUuid {
|
||||
@@ -370,6 +375,25 @@ export interface Change extends WithDates, WithUuid {
|
||||
user_id?: Uuid;
|
||||
}
|
||||
|
||||
export interface Email extends WithDates {
|
||||
id?: number;
|
||||
recipient_name?: string;
|
||||
recipient_email?: string;
|
||||
recipient_id?: Uuid;
|
||||
sender_id?: EmailSender;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
sent_time?: number;
|
||||
sent_success?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Token extends WithDates {
|
||||
id?: number;
|
||||
value?: string;
|
||||
user_id?: Uuid;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
users: {
|
||||
id: { type: 'string' },
|
||||
@@ -381,6 +405,10 @@ export const databaseSchema: DatabaseTables = {
|
||||
created_time: { type: 'string' },
|
||||
max_item_size: { type: 'number' },
|
||||
can_share: { type: 'number' },
|
||||
email_confirmed: { type: 'number' },
|
||||
must_set_password: { type: 'number' },
|
||||
account_type: { type: 'number' },
|
||||
can_upload: { type: 'number' },
|
||||
},
|
||||
sessions: {
|
||||
id: { type: 'string' },
|
||||
@@ -485,5 +513,26 @@ export const databaseSchema: DatabaseTables = {
|
||||
previous_item: { type: 'string' },
|
||||
user_id: { type: 'string' },
|
||||
},
|
||||
emails: {
|
||||
id: { type: 'number' },
|
||||
recipient_name: { type: 'string' },
|
||||
recipient_email: { type: 'string' },
|
||||
recipient_id: { type: 'string' },
|
||||
sender_id: { type: 'number' },
|
||||
subject: { type: 'string' },
|
||||
body: { type: 'string' },
|
||||
sent_time: { type: 'string' },
|
||||
sent_success: { type: 'number' },
|
||||
error: { type: 'string' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
tokens: {
|
||||
id: { type: 'number' },
|
||||
value: { type: 'string' },
|
||||
user_id: { type: 'string' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
@@ -5,6 +5,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
import config from '../config';
|
||||
import { NotificationKey } from '../models/NotificationModel';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
@@ -17,21 +18,12 @@ async function handleChangeAdminPasswordNotification(ctx: AppContext) {
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'change_admin_password',
|
||||
NotificationKey.ChangeAdminPassword,
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.models.user().profileUrl())
|
||||
);
|
||||
} else {
|
||||
await notificationModel.markAsRead(ctx.owner.id, 'change_admin_password');
|
||||
}
|
||||
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
);
|
||||
await notificationModel.markAsRead(ctx.owner.id, NotificationKey.ChangeAdminPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +35,7 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
NotificationKey.UsingSqliteInProd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,6 @@
|
||||
import { routeResponseFormat, Response, RouteResponseFormat, execRequest } from '../utils/routeUtils';
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import MustacheService, { isView, View } from '../services/MustacheService';
|
||||
import config from '../config';
|
||||
|
||||
let mustache_: MustacheService = null;
|
||||
function mustache(): MustacheService {
|
||||
if (!mustache_) {
|
||||
mustache_ = new MustacheService(config().viewDir, config().baseUrl);
|
||||
}
|
||||
return mustache_;
|
||||
}
|
||||
import { isView, View } from '../services/MustacheService';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
@@ -21,7 +12,7 @@ export default async function(ctx: AppContext) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustache().renderView(responseObject, {
|
||||
ctx.response.body = await ctx.services.mustache.renderView(responseObject, {
|
||||
notifications: ctx.notifications || [],
|
||||
hasNotifications: !!ctx.notifications && !!ctx.notifications.length,
|
||||
owner: ctx.owner,
|
||||
@@ -44,7 +35,9 @@ export default async function(ctx: AppContext) {
|
||||
|
||||
const responseFormat = routeResponseFormat(ctx);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
if (error.code === 'invalidOrigin') {
|
||||
ctx.response.body = error.message;
|
||||
} else if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
@@ -55,7 +48,7 @@ export default async function(ctx: AppContext) {
|
||||
owner: ctx.owner,
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustache().renderView(view);
|
||||
ctx.response.body = await ctx.services.mustache.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
|
47
packages/server/src/migrations/20210518172311_mailer.ts
Normal file
47
packages/server/src/migrations/20210518172311_mailer.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
|
||||
table.integer('email_confirmed').defaultTo(0).notNullable();
|
||||
table.integer('must_set_password').defaultTo(0).notNullable();
|
||||
});
|
||||
|
||||
await db.schema.createTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.increments('id').unique().primary().notNullable();
|
||||
table.text('recipient_name', 'mediumtext').defaultTo('').notNullable();
|
||||
table.text('recipient_email', 'mediumtext').defaultTo('').notNullable();
|
||||
table.string('recipient_id', 32).defaultTo(0).notNullable();
|
||||
table.integer('sender_id').notNullable();
|
||||
table.string('subject', 128).notNullable();
|
||||
table.text('body').notNullable();
|
||||
table.bigInteger('sent_time').defaultTo(0).notNullable();
|
||||
table.integer('sent_success').defaultTo(0).notNullable();
|
||||
table.text('error').defaultTo('').notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.createTable('tokens', function(table: Knex.CreateTableBuilder) {
|
||||
table.increments('id').unique().primary().notNullable();
|
||||
table.string('value', 32).notNullable();
|
||||
table.string('user_id', 32).defaultTo('').notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.index(['sent_time']);
|
||||
table.index(['sent_success']);
|
||||
});
|
||||
|
||||
await db('users').update({ email_confirmed: 1 });
|
||||
|
||||
await db.schema.alterTable('tokens', function(table: Knex.CreateTableBuilder) {
|
||||
table.index(['value', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
|
||||
table.integer('account_type').defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
}
|
14
packages/server/src/migrations/20210527161932_can_upload.ts
Normal file
14
packages/server/src/migrations/20210527161932_can_upload.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
|
||||
table.integer('can_upload').defaultTo(1).notNullable();
|
||||
});
|
||||
|
||||
await db('users').update({ can_upload: 1 });
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
}
|
@@ -97,7 +97,10 @@ export default abstract class BaseModel<T> {
|
||||
}
|
||||
|
||||
if (mainTable) {
|
||||
output = output.map(f => `${mainTable}.${f}`);
|
||||
output = output.map(f => {
|
||||
if (f.includes(`${mainTable}.`)) return f;
|
||||
return `${mainTable}.${f}`;
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -272,10 +275,10 @@ export default abstract class BaseModel<T> {
|
||||
return this.db(this.tableName).select(options.fields || this.defaultFields).where({ id: id }).first();
|
||||
}
|
||||
|
||||
public async delete(id: string | string[], options: DeleteOptions = {}): Promise<void> {
|
||||
public async delete(id: string | string[] | number | number[], options: DeleteOptions = {}): Promise<void> {
|
||||
if (!id) throw new Error('id cannot be empty');
|
||||
|
||||
const ids = typeof id === 'string' ? [id] : id;
|
||||
const ids = (typeof id === 'string' || typeof id === 'number') ? [id] : id;
|
||||
|
||||
if (!ids.length) throw new Error('no id provided');
|
||||
|
||||
|
@@ -68,7 +68,7 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
return results;
|
||||
}
|
||||
|
||||
private changesForUserQuery(userId: Uuid): Knex.QueryBuilder {
|
||||
private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
|
||||
// When need to get:
|
||||
//
|
||||
// - All the CREATE and DELETE changes associated with the user
|
||||
@@ -78,15 +78,8 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
// UPDATE changes do not have the user_id set because they are specific
|
||||
// to the item, not to a particular user.
|
||||
|
||||
return this
|
||||
const query = this
|
||||
.db('changes')
|
||||
.select([
|
||||
'id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
'type',
|
||||
'updated_time',
|
||||
])
|
||||
.where(function() {
|
||||
void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
|
||||
// Need to use a RAW query here because Knex has a "not a
|
||||
@@ -96,6 +89,20 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
// https://github.com/knex/knex/issues/1851
|
||||
.orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
|
||||
});
|
||||
|
||||
if (count) {
|
||||
void query.countDistinct('id', { as: 'total' });
|
||||
} else {
|
||||
void query.select([
|
||||
'id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
'type',
|
||||
'updated_time',
|
||||
]);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedChanges> {
|
||||
@@ -106,9 +113,9 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const query = this.changesForUserQuery(userId);
|
||||
const countQuery = query.clone();
|
||||
const itemCount = (await countQuery.countDistinct('id', { as: 'total' }))[0].total;
|
||||
const query = this.changesForUserQuery(userId, false);
|
||||
const countQuery = this.changesForUserQuery(userId, true);
|
||||
const itemCount = (await countQuery.first()).total;
|
||||
|
||||
void query
|
||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
@@ -140,7 +147,7 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
if (!changeAtCursor) throw new ErrorResyncRequired();
|
||||
}
|
||||
|
||||
const query = this.changesForUserQuery(userId);
|
||||
const query = this.changesForUserQuery(userId, false);
|
||||
|
||||
// If a cursor was provided, apply it to the query.
|
||||
if (changeAtCursor) {
|
||||
|
33
packages/server/src/models/EmailModel.ts
Normal file
33
packages/server/src/models/EmailModel.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Uuid, Email, EmailSender } from '../db';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export interface EmailToSend {
|
||||
sender_id: EmailSender;
|
||||
recipient_email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
|
||||
recipient_name?: string;
|
||||
recipient_id?: Uuid;
|
||||
}
|
||||
|
||||
export default class EmailModel extends BaseModel<Email> {
|
||||
|
||||
public get tableName(): string {
|
||||
return 'emails';
|
||||
}
|
||||
|
||||
protected hasUuid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async push(email: EmailToSend) {
|
||||
EmailModel.eventEmitter.emit('saved');
|
||||
return super.save({ ...email });
|
||||
}
|
||||
|
||||
public async needToBeSent(): Promise<Email[]> {
|
||||
return this.db(this.tableName).where('sent_time', '=', 0);
|
||||
}
|
||||
|
||||
}
|
@@ -130,4 +130,16 @@ describe('ItemModel', function() {
|
||||
}
|
||||
});
|
||||
|
||||
test('should count items', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1, true);
|
||||
|
||||
await createItemTree(user1.id, '', {
|
||||
'000000000000000000000000000000F1': {
|
||||
'00000000000000000000000000000001': null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await models().item().childrenCount(user1.id)).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -349,13 +349,18 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
}
|
||||
|
||||
|
||||
private childrenQuery(userId: Uuid, pathQuery: string = '', options: LoadOptions = {}): Knex.QueryBuilder {
|
||||
private childrenQuery(userId: Uuid, pathQuery: string = '', count: boolean = false, options: LoadOptions = {}): Knex.QueryBuilder {
|
||||
const query = this
|
||||
.db('user_items')
|
||||
.leftJoin('items', 'user_items.item_id', 'items.id')
|
||||
.select(this.selectFields(options, ['id', 'name', 'updated_time'], 'items'))
|
||||
.where('user_items.user_id', '=', userId);
|
||||
|
||||
if (count) {
|
||||
void query.countDistinct('items.id', { as: 'total' });
|
||||
} else {
|
||||
void query.select(this.selectFields(options, ['id', 'name', 'updated_time'], 'items'));
|
||||
}
|
||||
|
||||
if (pathQuery) {
|
||||
// We support /* as a prefix only. Anywhere else would have
|
||||
// performance issue or requires a revert index.
|
||||
@@ -376,14 +381,14 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
|
||||
public async children(userId: Uuid, pathQuery: string = '', pagination: Pagination = null, options: LoadOptions = {}): Promise<PaginatedItems> {
|
||||
pagination = pagination || defaultPagination();
|
||||
const query = this.childrenQuery(userId, pathQuery, options);
|
||||
const query = this.childrenQuery(userId, pathQuery, false, options);
|
||||
return paginateDbQuery(query, pagination, 'items');
|
||||
}
|
||||
|
||||
public async childrenCount(userId: Uuid, pathQuery: string = ''): Promise<number> {
|
||||
const query = this.childrenQuery(userId, pathQuery);
|
||||
const r = await query.countDistinct('items.id', { as: 'total' });
|
||||
return r[0].total;
|
||||
const query = this.childrenQuery(userId, pathQuery, true);
|
||||
const r = await query.first();
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
private async joplinItemPath(jopId: string): Promise<Item[]> {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { Notification, NotificationLevel } from '../db';
|
||||
import { NotificationKey } from './NotificationModel';
|
||||
|
||||
describe('NotificationModel', function() {
|
||||
|
||||
@@ -16,15 +17,15 @@ describe('NotificationModel', function() {
|
||||
});
|
||||
|
||||
test('should require a user to create the notification', async function() {
|
||||
await expectThrow(async () => models().notification().add('', 'test', NotificationLevel.Normal, 'test'));
|
||||
await expectThrow(async () => models().notification().add('', NotificationKey.ConfirmEmail, NotificationLevel.Normal, NotificationKey.ConfirmEmail));
|
||||
});
|
||||
|
||||
test('should create a notification', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey(user.id, 'test');
|
||||
expect(n.key).toBe('test');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey(user.id, NotificationKey.ConfirmEmail);
|
||||
expect(n.key).toBe(NotificationKey.ConfirmEmail);
|
||||
expect(n.message).toBe('testing');
|
||||
expect(n.level).toBe(NotificationLevel.Important);
|
||||
});
|
||||
@@ -32,18 +33,18 @@ describe('NotificationModel', function() {
|
||||
test('should create only one notification per key', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
expect((await model.all()).length).toBe(1);
|
||||
});
|
||||
|
||||
test('should mark a notification as read', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey(user.id, 'test')).read).toBe(0);
|
||||
await model.markAsRead(user.id, 'test');
|
||||
expect((await model.loadByKey(user.id, 'test')).read).toBe(1);
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(0);
|
||||
await model.markAsRead(user.id, NotificationKey.ConfirmEmail);
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -2,6 +2,38 @@ import { Notification, NotificationLevel, Uuid } from '../db';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import BaseModel, { ValidateOptions } from './BaseModel';
|
||||
|
||||
export enum NotificationKey {
|
||||
ConfirmEmail = 'confirmEmail',
|
||||
PasswordSet = 'passwordSet',
|
||||
EmailConfirmed = 'emailConfirmed',
|
||||
ChangeAdminPassword = 'change_admin_password',
|
||||
UsingSqliteInProd = 'using_sqlite_in_prod',
|
||||
}
|
||||
|
||||
interface NotificationType {
|
||||
level: NotificationLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const notificationTypes: Record<string, NotificationType> = {
|
||||
[NotificationKey.ConfirmEmail]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Welcome to Joplin Server! An email has been sent to you containing an activation link to complete your registration.',
|
||||
},
|
||||
[NotificationKey.EmailConfirmed]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'You email has been confirmed',
|
||||
},
|
||||
[NotificationKey.PasswordSet]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Welcome to Joplin Server! Your password has been set successfully.',
|
||||
},
|
||||
[NotificationKey.UsingSqliteInProd]: {
|
||||
level: NotificationLevel.Important,
|
||||
message: 'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.',
|
||||
},
|
||||
};
|
||||
|
||||
export default class NotificationModel extends BaseModel<Notification> {
|
||||
|
||||
protected get tableName(): string {
|
||||
@@ -13,13 +45,32 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
return super.validate(notification, options);
|
||||
}
|
||||
|
||||
public async add(userId: Uuid, key: string, level: NotificationLevel, message: string): Promise<Notification> {
|
||||
public async add(userId: Uuid, key: NotificationKey, level: NotificationLevel = null, message: string = null): Promise<Notification> {
|
||||
const n: Notification = await this.loadByKey(userId, key);
|
||||
if (n) return n;
|
||||
|
||||
const type = notificationTypes[key];
|
||||
|
||||
if (level === null) {
|
||||
if (type?.level) {
|
||||
level = type.level;
|
||||
} else {
|
||||
throw new Error('Missing notification level');
|
||||
}
|
||||
}
|
||||
|
||||
if (message === null) {
|
||||
if (type?.message) {
|
||||
message = type.message;
|
||||
} else {
|
||||
throw new Error('Missing notification message');
|
||||
}
|
||||
}
|
||||
|
||||
return this.save({ key, message, level, owner_id: userId });
|
||||
}
|
||||
|
||||
public async markAsRead(userId: Uuid, key: string): Promise<void> {
|
||||
public async markAsRead(userId: Uuid, key: NotificationKey): Promise<void> {
|
||||
const n = await this.loadByKey(userId, key);
|
||||
if (!n) return;
|
||||
|
||||
@@ -29,7 +80,7 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
.andWhere('owner_id', '=', userId);
|
||||
}
|
||||
|
||||
public loadByKey(userId: Uuid, key: string): Promise<Notification> {
|
||||
public loadByKey(userId: Uuid, key: NotificationKey): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('key', '=', key)
|
||||
|
30
packages/server/src/models/TokenModel.test.ts
Normal file
30
packages/server/src/models/TokenModel.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models } from '../utils/testing/testUtils';
|
||||
|
||||
describe('TokenModel', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('TokenModel');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should delete old tokens', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1);
|
||||
await models().token().generate(user1.id);
|
||||
|
||||
const [token1, token2] = await models().token().all();
|
||||
await models().token().save({ id: token1.id, created_time: Date.now() - 2629746000 });
|
||||
await models().token().deleteExpiredTokens();
|
||||
|
||||
const tokens = await models().token().all();
|
||||
expect(tokens.length).toBe(1);
|
||||
expect(tokens[0].id).toBe(token2.id);
|
||||
});
|
||||
|
||||
});
|
62
packages/server/src/models/TokenModel.ts
Normal file
62
packages/server/src/models/TokenModel.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Token, Uuid } from '../db';
|
||||
import { ErrorForbidden } from '../utils/errors';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export default class TokenModel extends BaseModel<Token> {
|
||||
|
||||
private tokenTtl_: number = 7 * 24 * 60 * 1000;
|
||||
|
||||
public get tableName(): string {
|
||||
return 'tokens';
|
||||
}
|
||||
|
||||
protected hasUuid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async generate(userId: Uuid): Promise<string> {
|
||||
const token = await this.save({
|
||||
value: uuidgen(32),
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
return token.value;
|
||||
}
|
||||
|
||||
public async checkToken(userId: string, tokenValue: string): Promise<void> {
|
||||
if (!(await this.isValid(userId, tokenValue))) throw new ErrorForbidden('Invalid or expired token');
|
||||
}
|
||||
|
||||
private async byUser(userId: string, tokenValue: string): Promise<Token> {
|
||||
return this
|
||||
.db(this.tableName)
|
||||
.select(['id'])
|
||||
.where('user_id', '=', userId)
|
||||
.where('value', '=', tokenValue)
|
||||
.first();
|
||||
}
|
||||
|
||||
public async isValid(userId: string, tokenValue: string): Promise<boolean> {
|
||||
const token = await this.byUser(userId, tokenValue);
|
||||
return !!token;
|
||||
}
|
||||
|
||||
public async deleteExpiredTokens() {
|
||||
const cutOffDate = Date.now() - this.tokenTtl_;
|
||||
await this.db(this.tableName).where('created_time', '<', cutOffDate).delete();
|
||||
}
|
||||
|
||||
public async deleteByValue(userId: Uuid, value: string) {
|
||||
const token = await this.byUser(userId, value);
|
||||
if (token) await this.delete(token.id);
|
||||
}
|
||||
|
||||
public async allByUserId(userId: Uuid): Promise<Token[]> {
|
||||
return this
|
||||
.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('user_id', '=', userId);
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, checkThrowAsync, createItem } from '../utils/testing/testUtils';
|
||||
import { User } from '../db';
|
||||
import { EmailSender, User } from '../db';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
|
||||
describe('UserModel', function() {
|
||||
@@ -68,4 +68,22 @@ describe('UserModel', function() {
|
||||
expect((await models().userItem().all()).length).toBe(0);
|
||||
});
|
||||
|
||||
test('should push an email when creating a new user', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1);
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
|
||||
const emails = await models().email().all();
|
||||
expect(emails.length).toBe(2);
|
||||
expect(emails.find(e => e.recipient_email === user1.email)).toBeTruthy();
|
||||
expect(emails.find(e => e.recipient_email === user2.email)).toBeTruthy();
|
||||
|
||||
const email = emails[0];
|
||||
expect(email.subject.trim()).toBeTruthy();
|
||||
expect(email.body.includes('/confirm?token=')).toBeTruthy();
|
||||
expect(email.sender_id).toBe(EmailSender.NoReply);
|
||||
expect(email.sent_success).toBe(0);
|
||||
expect(email.sent_time).toBe(0);
|
||||
expect(email.error).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import BaseModel, { AclAction, SaveOptions, ValidateOptions } from './BaseModel';
|
||||
import { Item, User } from '../db';
|
||||
import { EmailSender, Item, User, Uuid } from '../db';
|
||||
import * as auth from '../utils/auth';
|
||||
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge } from '../utils/errors';
|
||||
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge, ErrorNotFound } from '../utils/errors';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import prettyBytes = require('pretty-bytes');
|
||||
@@ -134,10 +134,14 @@ export default class UserModel extends BaseModel<User> {
|
||||
return !!s[0].length && !!s[1].length;
|
||||
}
|
||||
|
||||
public async profileUrl(): Promise<string> {
|
||||
public profileUrl(): string {
|
||||
return `${this.baseUrl}/users/me`;
|
||||
}
|
||||
|
||||
public confirmUrl(userId: Uuid, validationToken: string): string {
|
||||
return `${this.baseUrl}/users/${userId}/confirm?token=${validationToken}`;
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
const shares = await this.models().share().sharesByUser(id);
|
||||
|
||||
@@ -151,6 +155,13 @@ export default class UserModel extends BaseModel<User> {
|
||||
}, 'UserModel::delete');
|
||||
}
|
||||
|
||||
public async confirmEmail(userId: Uuid, token: string) {
|
||||
await this.models().token().checkToken(userId, token);
|
||||
const user = await this.models().user().load(userId);
|
||||
if (!user) throw new ErrorNotFound('No such user');
|
||||
await this.save({ id: user.id, email_confirmed: 1 });
|
||||
}
|
||||
|
||||
// Note that when the "password" property is provided, it is going to be
|
||||
// hashed automatically. It means that it is not safe to do:
|
||||
//
|
||||
@@ -160,8 +171,30 @@ export default class UserModel extends BaseModel<User> {
|
||||
// Because the password would be hashed twice.
|
||||
public async save(object: User, options: SaveOptions = {}): Promise<User> {
|
||||
const user = { ...object };
|
||||
|
||||
if (user.password) user.password = auth.hashPassword(user.password);
|
||||
return super.save(user, options);
|
||||
|
||||
const isNew = await this.isNew(object, options);
|
||||
|
||||
return this.withTransaction(async () => {
|
||||
const savedUser = await super.save(user, options);
|
||||
|
||||
if (isNew) {
|
||||
const validationToken = await this.models().token().generate(savedUser.id);
|
||||
const confirmUrl = encodeURI(this.confirmUrl(savedUser.id, validationToken));
|
||||
|
||||
await this.models().email().push({
|
||||
sender_id: EmailSender.NoReply,
|
||||
recipient_id: savedUser.id,
|
||||
recipient_email: savedUser.email,
|
||||
recipient_name: savedUser.full_name || '',
|
||||
subject: 'Please setup your Joplin account',
|
||||
body: `Your new Joplin account has been created!\n\nPlease click on the following link to complete the creation of your account:\n\n${confirmUrl}`,
|
||||
});
|
||||
}
|
||||
|
||||
return savedUser;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -63,9 +63,11 @@ import SessionModel from './SessionModel';
|
||||
import ChangeModel from './ChangeModel';
|
||||
import NotificationModel from './NotificationModel';
|
||||
import ShareModel from './ShareModel';
|
||||
import EmailModel from './EmailModel';
|
||||
import ItemResourceModel from './ItemResourceModel';
|
||||
import ShareUserModel from './ShareUserModel';
|
||||
import KeyValueModel from './KeyValueModel';
|
||||
import TokenModel from './TokenModel';
|
||||
|
||||
export class Models {
|
||||
|
||||
@@ -85,10 +87,18 @@ export class Models {
|
||||
return new UserModel(this.db_, newModelFactory, this.baseUrl_);
|
||||
}
|
||||
|
||||
public email() {
|
||||
return new EmailModel(this.db_, newModelFactory, this.baseUrl_);
|
||||
}
|
||||
|
||||
public userItem() {
|
||||
return new UserItemModel(this.db_, newModelFactory, this.baseUrl_);
|
||||
}
|
||||
|
||||
public token() {
|
||||
return new TokenModel(this.db_, newModelFactory, this.baseUrl_);
|
||||
}
|
||||
|
||||
public itemResource() {
|
||||
return new ItemResourceModel(this.db_, newModelFactory, this.baseUrl_);
|
||||
}
|
||||
|
@@ -2,10 +2,11 @@ import config from '../../config';
|
||||
import { createTestUsers } from '../../tools/debugTools';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
|
||||
@@ -14,7 +15,7 @@ const supportedEvents: Record<string, Function> = {
|
||||
},
|
||||
};
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.post('api/events', async (_path: SubPath, ctx: AppContext) => {
|
||||
const event = await bodyFields<Event>(ctx.req);
|
||||
|
@@ -276,4 +276,18 @@ describe('api_items', function() {
|
||||
}
|
||||
});
|
||||
|
||||
test('should check permissions - should not allow uploading items if disabled', async function() {
|
||||
const { user: user1, session: session1 } = await createUserAndSession(1);
|
||||
|
||||
await models().user().save({ id: user1.id, can_upload: 0 });
|
||||
|
||||
await expectHttpError(
|
||||
async () => createNote(session1.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
body: '12345',
|
||||
}),
|
||||
ErrorForbidden.httpCode
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -2,15 +2,16 @@ import { Item, Uuid } from '../../db';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import { respondWithItemContent, SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import * as fs from 'fs-extra';
|
||||
import { ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
|
||||
import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
|
||||
import ItemModel, { ItemSaveOption } from '../../models/ItemModel';
|
||||
import { requestDeltaPagination, requestPagination } from '../../models/utils/pagination';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
import { safeRemove } from '../../utils/fileUtils';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
// Note about access control:
|
||||
//
|
||||
@@ -65,6 +66,8 @@ router.get('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
|
||||
});
|
||||
|
||||
router.put('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
|
||||
if (!ctx.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled');
|
||||
|
||||
const itemModel = ctx.models.item();
|
||||
const name = itemModel.pathToName(path.id);
|
||||
const parsedBody = await formParse(ctx.req);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { User } from '../../db';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
@@ -2,10 +2,11 @@ import { ErrorBadRequest, ErrorNotFound } from '../../utils/errors';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
const shareUserModel = ctx.models.shareUser();
|
||||
|
@@ -3,6 +3,7 @@ import { Share, ShareType } from '../../db';
|
||||
import { bodyFields, ownerRequired } from '../../utils/requestUtils';
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
|
||||
@@ -11,7 +12,7 @@ interface ShareApiInput extends Share {
|
||||
note_id?: string;
|
||||
}
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
@@ -33,6 +33,8 @@ describe('api_users', function() {
|
||||
expect(savedUser.email).toBe('toto@example.com');
|
||||
expect(savedUser.can_share).toBe(0);
|
||||
expect(savedUser.max_item_size).toBe(1000);
|
||||
expect(savedUser.email_confirmed).toBe(0);
|
||||
expect(savedUser.must_set_password).toBe(1);
|
||||
});
|
||||
|
||||
test('should patch a user', async function() {
|
||||
|
@@ -2,12 +2,13 @@ import { User } from '../../db';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
async function fetchUser(path: SubPath, ctx: AppContext): Promise<User> {
|
||||
const user = await ctx.models.user().load(path.id);
|
||||
@@ -30,8 +31,10 @@ router.post('api/users', async (_path: SubPath, ctx: AppContext) => {
|
||||
const user = await postedUserFromContext(ctx);
|
||||
|
||||
// We set a random password because it's required, but user will have to
|
||||
// set it by clicking on the confirmation link.
|
||||
// set it after clicking on the confirmation link.
|
||||
user.password = uuidgen();
|
||||
user.must_set_password = 1;
|
||||
user.email_confirmed = 0;
|
||||
const output = await ctx.models.user().save(user);
|
||||
return ctx.models.user().toApiOutput(output);
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { SubPath, Response, ResponseType } from '../utils/routeUtils';
|
||||
import { SubPath, Response, ResponseType, redirect } from '../utils/routeUtils';
|
||||
import Router from '../utils/Router';
|
||||
import { ErrorNotFound, ErrorForbidden } from '../utils/errors';
|
||||
import { dirname, normalize } from 'path';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import * as fs from 'fs-extra';
|
||||
import { AppContext } from '../utils/types';
|
||||
import { AppContext, RouteType } from '../utils/types';
|
||||
import { localFileFromUrl } from '../utils/joplinUtils';
|
||||
const { mime } = require('@joplin/lib/mime-utils.js');
|
||||
|
||||
@@ -44,13 +44,22 @@ async function findLocalFile(path: string): Promise<string> {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.public = true;
|
||||
|
||||
// Used to serve static files, so it needs to be public because for example the
|
||||
// login page, which is public, needs access to the CSS files.
|
||||
router.get('', async (path: SubPath, ctx: AppContext) => {
|
||||
// Redirect to either /login or /home when trying to access the root
|
||||
if (!path.id && !path.link) {
|
||||
if (ctx.owner) {
|
||||
return redirect(ctx, 'home');
|
||||
} else {
|
||||
return redirect(ctx, 'login');
|
||||
}
|
||||
}
|
||||
|
||||
const localPath = await findLocalFile(path.raw);
|
||||
|
||||
let mimeType: string = mime.fromFilename(localPath);
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { changeTypeToString } from '../../db';
|
||||
import { PaginationOrderDir } from '../../models/utils/pagination';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import { makeTablePagination, Table, Row, makeTableView, tablePartials } from '../../utils/views/table';
|
||||
import { makeTablePagination, Table, Row, makeTableView } from '../../utils/views/table';
|
||||
import config, { showItemUrls } from '../../config';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.get('changes', async (_path: SubPath, ctx: AppContext) => {
|
||||
const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
|
||||
@@ -40,7 +42,7 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => {
|
||||
{
|
||||
value: change.item_name,
|
||||
stretch: true,
|
||||
url: items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '',
|
||||
url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '') : null,
|
||||
},
|
||||
{
|
||||
value: changeTypeToString(change.type),
|
||||
@@ -57,7 +59,6 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => {
|
||||
const view: View = defaultView('changes');
|
||||
view.content.changeTable = makeTableView(table),
|
||||
view.cssFiles = ['index/changes'];
|
||||
view.partials = view.partials.concat(tablePartials());
|
||||
return view;
|
||||
});
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { contextSessionId } from '../../utils/requestUtils';
|
||||
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
|
||||
const router: Router = new Router();
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.get('home', async (_path: SubPath, ctx: AppContext) => {
|
||||
contextSessionId(ctx);
|
||||
|
@@ -1,17 +1,18 @@
|
||||
import { SubPath, redirect, respondWithItemContent } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import config from '../../config';
|
||||
import config, { showItemUrls } from '../../config';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import { makeTablePagination, makeTableView, Row, Table, tablePartials } from '../../utils/views/table';
|
||||
import { makeTablePagination, makeTableView, Row, Table } from '../../utils/views/table';
|
||||
import { PaginationOrderDir } from '../../models/utils/pagination';
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.get('items', async (_path: SubPath, ctx: AppContext) => {
|
||||
const pagination = makeTablePagination(ctx.query, 'name', PaginationOrderDir.ASC);
|
||||
@@ -46,7 +47,7 @@ router.get('items', async (_path: SubPath, ctx: AppContext) => {
|
||||
{
|
||||
value: item.name,
|
||||
stretch: true,
|
||||
url: `${config().baseUrl}/items/${item.id}/content`,
|
||||
url: showItemUrls(config()) ? `${config().userContentBaseUrl}/items/${item.id}/content` : null,
|
||||
},
|
||||
{
|
||||
value: prettyBytes(item.content_size),
|
||||
@@ -67,7 +68,6 @@ router.get('items', async (_path: SubPath, ctx: AppContext) => {
|
||||
view.content.itemTable = makeTableView(table),
|
||||
view.content.postUrl = `${config().baseUrl}/items`;
|
||||
view.cssFiles = ['index/items'];
|
||||
view.partials = view.partials.concat(tablePartials());
|
||||
return view;
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ router.get('items/:id/content', async (path: SubPath, ctx: AppContext) => {
|
||||
const item = await itemModel.loadWithContent(path.id);
|
||||
if (!item) throw new ErrorNotFound();
|
||||
return respondWithItemContent(ctx.response, item, item.content);
|
||||
});
|
||||
}, RouteType.UserContent);
|
||||
|
||||
router.post('items', async (_path: SubPath, ctx: AppContext) => {
|
||||
const body = await formParse(ctx.req);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import config from '../../config';
|
||||
@@ -9,11 +10,11 @@ import { View } from '../../services/MustacheService';
|
||||
function makeView(error: any = null): View {
|
||||
const view = defaultView('login');
|
||||
view.content.error = error;
|
||||
view.partials = ['errorBanner'];
|
||||
view.navbar = false;
|
||||
return view;
|
||||
}
|
||||
|
||||
const router: Router = new Router();
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import config from '../../config';
|
||||
import { contextSessionId } from '../../utils/requestUtils';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
|
||||
const sessionId = contextSessionId(ctx, false);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { NotificationLevel } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('index_notification', function() {
|
||||
@@ -21,9 +22,9 @@ describe('index_notification', function() {
|
||||
|
||||
const model = models().notification();
|
||||
|
||||
await model.add(user.id, 'my_notification', NotificationLevel.Normal, 'testing notification');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Normal, 'testing notification');
|
||||
|
||||
const notification = await model.loadByKey(user.id, 'my_notification');
|
||||
const notification = await model.loadByKey(user.id, NotificationKey.ConfirmEmail);
|
||||
|
||||
expect(notification.read).toBe(0);
|
||||
|
||||
@@ -40,7 +41,7 @@ describe('index_notification', function() {
|
||||
|
||||
await routeHandler(context);
|
||||
|
||||
expect((await model.loadByKey(user.id, 'my_notification')).read).toBe(1);
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import { Notification } from '../../db';
|
||||
|
||||
const router = new Router();
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.patch('notifications/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
const fields: Notification = await bodyFields(ctx.req);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { SubPath, ResponseType, Response } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import { Item, Share } from '../../db';
|
||||
@@ -18,7 +19,7 @@ async function renderItem(context: AppContext, item: Item, share: Share): Promis
|
||||
};
|
||||
}
|
||||
|
||||
const router: Router = new Router();
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.public = true;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user