1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

..

300 Commits

Author SHA1 Message Date
Laurent Cozic
dce8bced15 Server v2.13.3 2023-11-01 16:30:12 +00:00
Laurent Cozic
a9719307af lock files 2023-11-01 16:29:04 +00:00
Laurent Cozic
0c8b475736 Server: Automatically restarts the server when it crashes, with exponentiel back-off 2023-11-01 16:28:45 +00:00
Laurent Cozic
dbd3db873f Fix ts error 2023-11-01 16:25:46 +00:00
Laurent Cozic
073781da92 Chore: Fixed attachFile method 2023-11-01 14:58:21 +00:00
Laurent Cozic
7d87d0b394 Chore: Remove postinstall step from editor package 2023-11-01 14:54:48 +00:00
renovate[bot]
0cb2a3a385 Update eslint (#9184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 10:48:09 +00:00
renovate[bot]
c1e970a703 Update dependency @react-native-community/datetimepicker to v7.6.1 (#9181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 07:16:07 +00:00
renovate[bot]
17831bf87a Update electron (#9183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 03:32:57 +00:00
Joplin Bot
a93c558479 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-01 00:41:29 +00:00
Laurent Cozic
13b09aa9a4 Doc: Set Data API link position 2023-10-31 20:39:52 +00:00
Joplin Bot
703fe35121 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 18:18:53 +00:00
Laurent Cozic
a7dddaf2c4 Desktop: Allow attaching a file from the Markdown editor for HTML notes 2023-10-31 16:53:47 +00:00
Laurent Cozic
a95a66104d Doc: Fixed desktop changelog generation 2023-10-31 15:59:58 +00:00
Laurent Cozic
dd47571dff Plugins: Add support for showOpenDialog method 2023-10-31 15:30:05 +00:00
Laurent Cozic
0a2c3b3a91 Doc: Add config screen link to E2EE doc 2023-10-31 15:30:04 +00:00
Henry Heino
694ca6480e Desktop: Resolves #8742: Prompt to restart in safe mode on renderer process hang/crash (#9153) 2023-10-31 15:05:28 +00:00
Henry Heino
86b00d0a2b Mobile: Resolves #9134: Image editor: Allow loading from save when the image editor is reloaded in the background (#9135) 2023-10-31 14:57:26 +00:00
Laurent Cozic
b8c26b2ef3 Doc: Fixed Crowdin translation build 2023-10-31 14:06:54 +00:00
renovate[bot]
3343c2b0aa Update dependency sass to v1.68.0 (#9177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 12:28:51 +00:00
Joplin Bot
f9c60bd47b Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 11:47:05 +00:00
Piotr Narel
71d2256fb7 All: Translation: Update pl_PL.po (#9174) 2023-10-31 07:46:36 -04:00
Laurent Cozic
66fe2f9390 Doc: Add support for localisation using Crowdin 2023-10-31 11:32:55 +00:00
renovate[bot]
3fe473cdd3 Update dependency @types/js-yaml to v4.0.7 (#9173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 06:53:03 +00:00
Joplin Bot
3255f4d63b Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 00:40:05 +00:00
Laurent Cozic
66036c2027 Desktop release v2.13.4 2023-10-30 22:50:51 +00:00
Laurent Cozic
3b98060c27 Android 2.13.5 2023-10-30 22:50:30 +00:00
Laurent Cozic
972f468527 iOS 12.13.3 2023-10-30 22:21:31 +00:00
Laurent Cozic
e80479acdc lock file 2023-10-30 22:20:49 +00:00
Laurent Cozic
77b55a40bb Doc: Fixes #9171: Fixed data API doc 2023-10-30 22:18:14 +00:00
Henry Heino
94cef15d9f Mobile: Fixes #9175: Beta editor: Fix image timestamps not updated after editing (#9176) 2023-10-30 22:17:49 +00:00
renovate[bot]
2d71d01beb Update dependency @types/markdown-it to v13.0.2 (#9145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 19:46:11 +00:00
Laurent Cozic
c11c6efb64 Ignore certain paths with renovate 2023-10-30 19:41:49 +00:00
Laurent Cozic
6706960f5a Doc: Set favicon 2023-10-30 18:22:31 +00:00
Laurent Cozic
fb287f2525 Doc: Fixed links 2023-10-30 18:22:31 +00:00
Joplin Bot
39194d998e Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-30 18:21:48 +00:00
renovate[bot]
0c2db4a6fb Update dependency terminal-kit to v3.0.1 (#9166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 16:43:32 +00:00
Laurent Cozic
467730b689 Doc: Add Twitter and other metadata tags 2023-10-30 12:57:56 +00:00
Laurent Cozic
0915ef768e Doc: Added a page for the Plugin API reference 2023-10-30 12:28:55 +00:00
Laurent Cozic
09e3377e6b Doc: Fixed link targets 2023-10-30 12:21:46 +00:00
Laurent Cozic
f2061ba34d Doc: Fixed dead links 2023-10-30 12:13:53 +00:00
Joplin Bot
a7d83c410e Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-30 11:44:30 +00:00
Laurent Cozic
5f6370d7ba Doc: Refactored documentation, split it into smaller articles, and added search (#9132) 2023-10-30 11:32:14 +00:00
renovate[bot]
f94cc0fe3c Update dependency react-native-device-info to v10.11.0 (#9164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 20:35:45 +00:00
renovate[bot]
e64951bfbf Update dependency react-native-device-info to v10.10.0 (#9163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 15:45:20 +00:00
renovate[bot]
14e87836f6 Update dependency nodemailer to v6.9.6 (#9161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 12:54:58 +00:00
Laurent Cozic
db88dfc17d Server: Improve parsing of uploaded content and error handling 2023-10-29 11:19:20 +00:00
Laurent Cozic
99dbb97aa4 lock file 2023-10-29 11:03:12 +00:00
renovate[bot]
49c1c9aa65 Update dependency @react-native-community/datetimepicker to v7.6.0 (#9155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-28 23:28:35 +01:00
github-actions[bot]
bab0cfdf81 @orl0 has signed the CLA in laurent22/joplin#9154 2023-10-28 11:30:39 +00:00
Piotr Narel
329c9d0127 All: Translation: Update pl_PL.po (#9095) 2023-10-27 18:01:18 -04:00
github-actions[bot]
9e650950e0 @marcorombach has signed the CLA in laurent22/joplin#9150 2023-10-27 10:53:09 +00:00
renovate[bot]
74f57f80f8 Update dependency @types/uuid to v9.0.5 (#9146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 07:37:45 +00:00
renovate[bot]
61c2178ff4 Update dependency @types/yargs to v17.0.28 (#9147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 04:39:36 +00:00
renovate[bot]
31e633d6bd Update dependency @types/markdown-it to v13 (#9144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 00:08:39 +01:00
renovate[bot]
a1e807151e Update dependency react-native-webview to v13.6.2 (#9139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 19:47:25 +00:00
renovate[bot]
b4f8958c03 Update dependency @codemirror/autocomplete to v6.9.2 (#9141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 16:43:04 +00:00
Cicero Tiago
5dccc49531 All: Translation: Update pt_BR.po (#9129) 2023-10-26 10:44:50 -04:00
Arda Kılıçdağı
2f6efa3fec All: Translation: Update tr_TR.po (#9102) 2023-10-26 09:49:50 -04:00
Laurent Cozic
62281f1b46 Chore: Retry feature test 2023-10-26 13:21:05 +01:00
Laurent Cozic
3667bf3ed9 All: Allow searching by note ID or using a callback URL 2023-10-25 14:43:22 +01:00
CptMeetKat
6b2e577be6 Desktop: Fixes #8916: Save to file after keyboard shortcuts are imported (#9128) 2023-10-25 14:43:11 +01:00
Laurent Cozic
6392a1e00f Chore: Fixed search engine result type 2023-10-25 14:10:35 +01:00
github-actions[bot]
b7c85e1930 @cicerotcv has signed the CLA in laurent22/joplin#9129 2023-10-25 07:21:50 +00:00
Laurent Cozic
0638922ae4 iOS 12.13.2 2023-10-24 19:41:16 +01:00
Laurent Cozic
4aa2cc689f Android 2.13.4 2023-10-24 19:39:04 +01:00
Laurent Cozic
6c74a4d3e4 Chore: Fixed broken JSX 2023-10-24 19:19:09 +01:00
Laurent Cozic
cb69b5192c Android 2.13.3 2023-10-24 18:53:50 +01:00
Laurent Cozic
d0a674957e iOS 12.13.1 2023-10-24 15:51:26 +01:00
Laurent Cozic
03e0bd76a9 build files 2023-10-24 15:49:48 +01:00
renovate[bot]
cc19b2bd18 Update react monorepo (#9112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-24 14:30:27 +01:00
Joplin Bot
e09f99aeac Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-24 12:24:10 +00:00
Laurent Cozic
11eead1cd5 Desktop: Automatically create a bug report when import faild, and allow uploading it to the forum 2023-10-24 12:20:54 +01:00
Laurent Cozic
df9db9c702 Desktop: Make sure that the import screen is cleared even when import fails 2023-10-24 11:28:14 +01:00
Henry Heino
388b9b681d Desktop: Fixes #9115: Beta editor: Improve performance on large documents (#9116) 2023-10-24 10:47:19 +01:00
renovate[bot]
5e01f82474 Update dependency nan to v2.18.0 (#9113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-24 10:46:52 +01:00
Laurent Cozic
9aed3e04f4 All: Allow modifying a resource metadata only when synchronising (#9114) 2023-10-24 10:46:33 +01:00
renovate[bot]
0069069254 Update dependency sass to v1.67.0 (#9125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-24 10:26:59 +01:00
Joplin Bot
773e02bcd0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-23 18:19:05 +00:00
Laurent Cozic
6f9ba953a6 Desktop release v2.13.3 2023-10-23 18:23:18 +01:00
Laurent Cozic
5bd45abc10 Chore: Fix new item title 2023-10-23 18:23:05 +01:00
Laurent Cozic
c1a18bac6b Doc: Update Joplin Cloud storage info to 2, 30, 50 GB 2023-10-23 18:18:36 +01:00
Laurent Cozic
e45835ed9a Doc: Added news item about white hat hackers 2023-10-23 17:23:22 +01:00
Laurent Cozic
154619cc42 Chore: Fixed fetching user logic for Joplin Cloud 2023-10-23 16:58:50 +01:00
pedr
a77462f8ea Chore: All: Add property to Setting to control if file storage is used to save the configuration (#9121) 2023-10-23 16:58:20 +01:00
Joplin Bot
909776c666 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-23 12:23:23 +00:00
CptMeetKat
cd8b5388ec Desktop: Fixes #9108: Added Note Properties to Note menu bar items (#9119) 2023-10-23 13:05:06 +01:00
renovate[bot]
4ecc4816e9 Update dependency katex to v0.16.9 (#9117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-23 00:20:13 +00:00
renovate[bot]
edda92c055 Update dependency react-select to v5.7.7 (#9109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 13:53:37 +00:00
Henry Heino
03bd77c107 Desktop: Resolves #8749: Use plain text editor in safe mode (#8750) 2023-10-22 12:00:19 +01:00
Henry Heino
c2bfc526e7 Desktop: Fixes #9070: Fix external links in PDFs break Joplin (#9094) 2023-10-22 11:52:06 +01:00
Henry Heino
0e6891fd88 Desktop: Fixes #9104: Beta editor: Allow tab key to insert tabs at cursor rather than indent in some cases (#9107) 2023-10-22 11:51:54 +01:00
Henry Heino
d3744b0e6e Mobile: Fixes #9066: Improve list toggle logic (#9103) 2023-10-22 11:51:46 +01:00
Henry Heino
6b319f4738 Mobile: Fixes #9069: Fix writing UTF-8 data to a file replaces non-ASCII characters with ?s (#9076) 2023-10-22 11:51:31 +01:00
renovate[bot]
39c336a5d8 Update dependency @types/react to v18.2.24 (#9100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-10-22 11:26:03 +01:00
renovate[bot]
1d4ea3d99f Update dependency react-native-dropdownalert to v5 (#9096)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-10-22 10:45:05 +01:00
Laurent Cozic
cc230e51ed Doc: Add more info to sync spec 2023-10-21 19:10:31 +01:00
Laurent Cozic
3fbdb8307c Fixed tests
Don't know how it ever worked
2023-10-21 17:00:01 +01:00
Laurent Cozic
8d20aa0bb8 Fixed types 2023-10-21 16:07:44 +01:00
Laurent Cozic
63aea35e36 Chore: All: Improve how migrations are handled 2023-10-21 15:53:17 +01:00
Joplin Bot
240f9a3ff0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-21 12:23:08 +00:00
Laurent Cozic
7a4eb7313e lock file 2023-10-21 09:44:07 +01:00
Laurent Cozic
90832daa90 Desktop: Update Electron to 25.9.0 2023-10-21 09:43:58 +01:00
Laurent Cozic
1c7d22eda3 All: Fixed issues related to sharing notes on read-only notebooks 2023-10-21 09:03:42 +01:00
renovate[bot]
c2d3c5baa4 Update dependency react-redux to v8.1.3 (#9099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 07:07:33 +00:00
Laurent Cozic
21f9189000 Server v2.13.2 2023-10-19 20:50:02 +01:00
github-actions[bot]
d92032e634 @PiotrNarel has signed the CLA in laurent22/joplin#9095 2023-10-19 17:56:16 +00:00
Laurent Cozic
5986710fc0 Server: Significantly improve sync performances, especially when there are many changes 2023-10-19 17:46:58 +01:00
Laurent Cozic
4d1e0cc21b Chore: Server: Added test tools to automatically populate the database (#9085) 2023-10-19 17:11:20 +01:00
renovate[bot]
7b42211581 Update dependency @types/yargs to v17.0.26 (#9092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-19 03:34:29 +00:00
renovate[bot]
2fc7bcec06 Update dependency react-native-webview to v13.6.0 (#9089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 12:38:13 +01:00
renovate[bot]
3e3d01d93e Update dependency tap to v16.3.9 (#9087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 06:14:03 +00:00
renovate[bot]
f634a1c731 Update dependency @types/react-redux to v7.1.27 (#9079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 03:42:05 +00:00
renovate[bot]
1fe91b4808 Update dependency @types/react-dom to v18.2.8 (#9078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 02:29:39 +00:00
renovate[bot]
d20c48855c Update dependency @types/markdown-it to v13.0.2 (#9082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 02:01:17 +00:00
renovate[bot]
38d310c0ad Update dependency @react-native-community/geolocation to v3.1.0 (#9088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 22:22:39 +01:00
pedr
c06ca87573 Chore: Export extractNoteFromHTML from notes services library (#9086) 2023-10-17 21:10:18 +01:00
renovate[bot]
d50d940f3c Update dependency @types/mustache to v4.2.3 (#9083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 16:26:04 +00:00
Joplin Bot
bde74d1f97 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-17 12:22:02 +00:00
Laurent Cozic
11f7915a54 Doc: Fixed typo 2023-10-17 11:24:57 +01:00
renovate[bot]
4501ecff3b Update dependency react-native-image-picker to v5.7.0 (#9077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 09:50:25 +01:00
renovate[bot]
387ba2c50f Update dependency markdown-it to v13.0.2 (#9073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 21:20:44 +00:00
renovate[bot]
7e085ef0bc Update dependency @testing-library/react-native to v12.3.0 (#9074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 21:39:21 +01:00
renovate[bot]
cfb1f11956 Update dependency @types/react to v18.2.23 (#9071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-16 19:18:49 +00:00
renovate[bot]
6668a52478 Update dependency @types/styled-components to v5.1.28 (#9067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 21:40:24 +00:00
renovate[bot]
92230fce72 Update dependency react-native-zip-archive to v6.1.0 (#9068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 20:04:26 +01:00
Laurent Cozic
f2269e9820 Chore: Fixed node-glob issues 2023-10-15 18:51:37 +01:00
Laurent Cozic
fbef66b65a Tools: Add node-glob to Renovate ignore list 2023-10-15 13:52:13 +01:00
renovate[bot]
8795da83d2 Update dependency @types/yargs to v17.0.25 (#9064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 10:46:21 +00:00
renovate[bot]
79f9b93b58 Update dependency tar to v6.2.0 (#9065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 10:28:52 +01:00
Helmut K. C. Tessarek
5fd4c19b8d All: Translation: Update da_DK.po (thanks ERYpTION) 2023-10-14 05:58:13 -04:00
renovate[bot]
7621dde8e7 Update dependency @types/nodemailer to v6.4.11 (#9062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 09:16:58 +00:00
renovate[bot]
02a797abe9 Update dependency @types/node-rsa to v1.1.2 (#9061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 06:30:24 +00:00
renovate[bot]
41d4734bd3 Update dependency @types/node-fetch to v2.6.6 (#9060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 03:50:59 +00:00
renovate[bot]
0c91e2c947 Update dependency @types/node to v18.17.19 (#9059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-14 02:03:04 +00:00
Henry Heino
2d06fd9d13 Chore: Desktop: Set up integration testing with Playwright (#9043) 2023-10-13 17:32:10 +03:00
pedr
5733017637 Chore: Move useful clipper logic to the lib package to be used in other places (#9053) 2023-10-13 17:31:13 +03:00
renovate[bot]
b1e1db7831 Update dependency glob to v10.3.6 (#9057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-13 03:21:27 +00:00
pedr
c5c03ab04e Chore: Improve isNode to be more inclusive (#9054) 2023-10-12 15:27:48 +03:00
Laurent Cozic
5cecfde085 Update translations 2023-10-12 15:26:55 +03:00
Joplin Bot
0c701f59c7 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-12 12:23:15 +00:00
Joplin Bot
05a4affd5a Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-12 06:21:15 +00:00
renovate[bot]
84ff840c15 Update dependency react-native-share to v9.4.1 (#9052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 15:43:00 +00:00
Laurent Cozic
4989d402a4 Fix config 2023-10-11 16:13:19 +03:00
Laurent Cozic
3ac25104c3 Merge branch 'dev' of github.com:laurent22/joplin into dev 2023-10-11 13:05:30 +03:00
renovate[bot]
2a73010c9d Update dependency react-native-share to v9.4.0 (#9050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 12:46:20 +03:00
Henry Heino
0f005c5039 Desktop: Fixes #8881: Fix markdown editor context menu not displaying on some devices (#9030) 2023-10-11 10:18:32 +01:00
Laurent Cozic
0402fa624d All: Support for plural translations (#9033) 2023-10-11 10:17:46 +01:00
renovate[bot]
a98c323bf3 Update dependency react-select to v5.7.5 (#9048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 09:16:28 +00:00
Laurent Cozic
d975d8d626 Tools: Auto-apply Electron minor and patch updates 2023-10-11 10:15:22 +01:00
renovate[bot]
19c694760f Update dependency react-native-paper to v5.10.6 (#9047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 07:25:54 +00:00
renovate[bot]
9236f68016 Update dependency glob to v10.3.5 (#9046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-11 04:20:09 +00:00
renovate[bot]
a052983fb6 Update dependency @types/react to v18.2.22 (#9044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 16:43:35 +00:00
renovate[bot]
089c6afd1a Update react monorepo (#9032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 13:59:23 +03:00
renovate[bot]
4b7f807b5b Update dependency react-native-webview to v13.5.1 (#9029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 13:59:03 +03:00
renovate[bot]
d4ccf06f98 Update dependency @types/node to v18.17.18 (#9042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 07:34:31 +00:00
renovate[bot]
2d9980dd36 Update dependency follow-redirects to v1.15.3 (#9041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-10 00:47:17 +00:00
renovate[bot]
db4e08b757 Update dependency dayjs to v1.11.10 (#9040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 18:44:28 +00:00
renovate[bot]
10cef6e146 Update dependency react-native-share to v9.3.0 (#9038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 19:40:55 +03:00
renovate[bot]
d33df54969 Update dependency sharp to v0.32.6 (#9035)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 04:10:07 +00:00
renovate[bot]
90feb65b44 Update dependency react-native-paper to v5.10.5 (#9034)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-09 01:21:47 +00:00
Laurent Cozic
cbe260eed2 Chore: Convert build-translations to TS 2023-10-08 19:20:53 +01:00
renovate[bot]
2f7801a267 Update dependency @react-native-community/datetimepicker to v7.5.0 (#9031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-08 18:57:07 +01:00
renovate[bot]
d58f62ca9d Update contributor-assistant/github-action action to v2.3.1 (#9025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-08 01:57:38 +00:00
renovate[bot]
d6f272c74f Update dependency react-native-device-info to v10.9.0 (#9026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-07 22:23:08 +01:00
Laurent Cozic
cd55a9a40f Mobile: Fix sidebar folder icon 2023-10-07 19:48:38 +01:00
Laurent Cozic
5cb54a57ac Android 2.13.2 2023-10-07 18:09:36 +01:00
Laurent Cozic
0e0c1d8395 Mobile: Fix icon after react-native-vector-icon upgrade 2023-10-07 17:25:03 +01:00
Laurent Cozic
26362146bc Android 2.13.1 2023-10-07 14:54:01 +01:00
Laurent Cozic
1b740a76cf fix ts errors 2023-10-06 21:19:57 +01:00
Laurent Cozic
b1461d699f Doc: Handle archived prices 2023-10-06 20:53:23 +01:00
Laurent Cozic
33c64f98ef Doc: Mention custom banner in Joplin Cloud page 2023-10-06 20:33:25 +01:00
Joplin Bot
822716c146 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-06 18:18:42 +00:00
Laurent Cozic
520d9df90f Plugin Generator release v2.13.1 2023-10-06 18:28:30 +01:00
Laurent Cozic
da40036a34 Doc: Fixed plugin doc 2023-10-06 18:27:40 +01:00
Laurent Cozic
3ac2fe94a4 Plugins: Fix building plugins on Windows 2023-10-06 18:05:41 +01:00
Laurent Cozic
789aaa6db6 Desktop release v2.13.2 2023-10-06 17:08:59 +01:00
Laurent Cozic
20374b6817 lock files 2023-10-06 17:08:14 +01:00
Laurent Cozic
e49ca26a36 API: Allow accessing revision data 2023-10-06 15:03:32 +01:00
renovate[bot]
634fa8a61f Update dependency @types/node to v18.17.17 (#9022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-06 11:12:41 +00:00
renovate[bot]
7372a2f928 Update dependency @types/jsdom to v21.1.3 (#9020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-06 03:55:51 +00:00
renovate[bot]
2b381badb2 Update dependency @types/js-yaml to v4.0.6 (#9019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-06 01:45:00 +00:00
renovate[bot]
006cf970ad Update dependency react-native-vector-icons to v10 (#9016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 18:28:48 +01:00
renovate[bot]
70347a733b Update dependency @types/node to v18.17.16 (#9015)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 12:57:05 +00:00
Self Not Found
b8aab34d1b Android: Fixed #8533: Fix sharing image to Joplin (#8970) 2023-10-05 11:51:46 +01:00
CptMeetKat
ef6d94973c Desktop: Fixes #8937: Preserve previous values when updating a note's date fields (#8948) 2023-10-05 11:50:07 +01:00
CptMeetKat
0320d9f479 Desktop: Fixes #8946: Whitelisted Joplin markdown links for copy pasting in WYSIWYG mode (#8972) 2023-10-05 11:49:25 +01:00
Jens Böttge
e70f31baea All: Enable ignoreTlsErrors and custom certificates for S3 sync (#8980) 2023-10-05 10:56:25 +01:00
Henry Heino
992807eb8f Chore: Fixes #9001: Fix error logged to console when a partial link is partially selected in Beta Editor (#9002) 2023-10-05 10:55:24 +01:00
Henry Heino
feb95451fd Desktop: Fixes #9003: Fix pasting text into Beta Editor with Cmd+V/Ctrl+V (#9004) 2023-10-05 10:54:10 +01:00
renovate[bot]
85f1fe259e Update dependency react-native-webview to v13.4.0 (#9013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 10:53:22 +01:00
renovate[bot]
8eccc0ca4d Update dependency url to v0.11.3 (#9012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 03:35:26 +00:00
renovate[bot]
ebd0d79b33 Update dependency @types/uuid to v9.0.4 (#9010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 00:03:23 +00:00
renovate[bot]
956578417b Update dependency @types/fs-extra to v11.0.2 (#9011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-04 23:28:29 +00:00
renovate[bot]
cc650d2adc Update dependency @types/node-fetch to v2.6.5 (#9008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-04 12:51:10 +00:00
renovate[bot]
edd081f4ed Update dependency @codemirror/autocomplete to v6.9.1 (#9009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-04 10:39:25 +00:00
renovate[bot]
9fca885246 Update dependency @types/koa to v2.13.9 (#9007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-04 07:09:58 +00:00
Hicham El Bouaaichi
4658b97d85 All: Translation: Update ar.po (#8959) 2023-10-02 15:15:13 -04:00
Henry Heino
53c82434be Mobile: Add share button to log screen (#8364)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-10-02 11:14:08 -07:00
renovate[bot]
f2d058dceb Update dependency @types/tar-stream to v2.2.3 (#9000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 16:06:48 +00:00
Henry Heino
849427d1bf Mobile: Add support for drawing pictures (#7588) 2023-10-02 15:15:51 +01:00
renovate[bot]
487112fd4d Update dependency uuid to v9.0.1 (#8998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 11:41:52 +00:00
renovate[bot]
7cb3357f97 Update dependency react-native-url-polyfill to v2 (#8997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 11:51:38 +01:00
Laurent Cozic
f504cf1a83 All: Fix search engine ranking algorithm 2023-10-02 10:42:13 +01:00
renovate[bot]
3585412fcb Update dependency sprintf-js to v1.1.3 (#8995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 19:32:55 +00:00
renovate[bot]
37efd2d91f Update dependency katex to v0.16.8 (#8993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 12:59:51 +01:00
Laurent Cozic
597e3d4b67 CI: Setup katex and mermaid so that they can be updated by Renovate 2023-10-01 11:05:52 +01:00
renovate[bot]
c65fa3087f Update eslint (major) (#8990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 10:00:00 +00:00
renovate[bot]
746b291d2a Update dependency @types/styled-components to v5.1.27 (#8992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 09:01:56 +00:00
renovate[bot]
d5e63aa904 Update dependency @types/nodemailer to v6.4.10 (#8991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 07:12:25 +00:00
renovate[bot]
36a235deb8 Update eslint (#8989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 03:59:27 +00:00
Joplin Bot
c8f1699caa Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-01 00:44:48 +00:00
renovate[bot]
59952d486a Update dependency url to v0.11.2 (#8981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-29 04:30:40 +00:00
renovate[bot]
04395976db Update dependency @types/node to v18.17.15 (#8983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-29 01:28:12 +00:00
github-actions[bot]
677d668402 @J-U-B has signed the CLA in laurent22/joplin#8980 2023-09-28 16:40:31 +00:00
renovate[bot]
608767fe22 Update dependency compare-versions to v6.1.0 (#8969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-27 10:28:20 +01:00
renovate[bot]
823970cfd7 Update dependency sass to v1.66.1 (#8968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-27 06:18:49 +00:00
renovate[bot]
a9bcf05b44 Update dependency nodemon to v3.0.1 (#8966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-27 04:14:38 +00:00
renovate[bot]
03ad00b2cd Update dependency compare-versions to v6 (#8964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-09-27 00:01:52 +01:00
renovate[bot]
84d18429d2 Update dependency sass to v1.66.0 (#8967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-27 00:01:34 +01:00
renovate[bot]
d0fc1d3ac3 Update dependency nodemon to v3 (#8965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-26 22:43:49 +01:00
renovate[bot]
ade67475fd Update dependency nodemailer to v6.9.5 (#8962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-26 10:01:19 +00:00
Joplin Bot
d22abe69b6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-09-25 18:17:59 +00:00
Henry Heino
b6c3dc32ad Mobile: Fixes #8956: Fix unordered list button creates checklists (#8957) 2023-09-25 16:45:25 +01:00
Laurent Cozic
d71a404636 Fixed tests 2023-09-25 16:26:50 +01:00
Laurent Cozic
34c4b832ba Revert: Desktop: Fixes #5955: Changing the currently opened note from plugins or the data API does not refresh the note content
Causes an infinite rendering loop when creating a new note
2023-09-25 16:16:07 +01:00
Laurent Cozic
1f19072f8f Revert "Desktop: Resolves #8817: Scrollbar thumb is barely visible in Light theme (#8831)"
This reverts commit c824f25262.
2023-09-25 15:34:39 +01:00
Laurent Cozic
c88cabe47a Clipper: Update library used to clip simplified pages 2023-09-25 15:29:42 +01:00
Laurent Cozic
52effd7760 Desktop, Cli: Fixes #8823: Ensure that ENEX resources with invalid filenames are imported correctly 2023-09-25 14:40:56 +01:00
Laurent Cozic
5ab6a89046 Desktop, Cli: Fixes #8802: Improved import of invalid Markdown+FrontMatter files 2023-09-25 13:47:49 +01:00
Laurent Cozic
1ea61c8505 Desktop, Cli: Fixes #8363: ENEX files that contain resources with invalid mime types are imported correctly 2023-09-25 12:25:55 +01:00
renovate[bot]
7f34e3f2f4 Update dependency react-native-paper to v5.10.4 (#8951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-25 04:45:11 +00:00
Laurent Cozic
fb9974c76f Desktop: Fixes #8305: Ensure that notes that contain invalid links can still be exported as HTML or PDF 2023-09-24 23:58:58 +01:00
Laurent Cozic
329f5a0dda Desktop: Resolves #8225: No strikethrough on completed to-dos in note list to improve readability 2023-09-24 23:38:20 +01:00
Laurent Cozic
9f14e61aff Mobile: Fixes #8050: Fix complex queries that contain quotes or filters 2023-09-24 23:22:36 +01:00
Laurent Cozic
d14b694b6c Desktop, Mobile: Resolves #8021: Remember whether "All notes", a notebook or a tag was opened when re-opening the app 2023-09-24 20:21:58 +01:00
Laurent Cozic
24097edc20 Tools: Trying to fix memory allocation crash on CI 2023-09-24 20:03:00 +01:00
Laurent Cozic
3a4942a5a4 Tools: Trying to fix memory allocation crash on CI 2023-09-24 19:01:19 +01:00
Laurent Cozic
24952c2f4a Tools: Trying to fix memory allocation crash on CI 2023-09-24 17:06:32 +01:00
Laurent Cozic
dd25f77393 Clipper: Fixes #7925: Fixes certain pages that may end up invisible once in Joplin 2023-09-24 16:58:14 +01:00
Laurent Cozic
bcf054fd08 All: Resolves #6978: Improved handling of invalid sync info 2023-09-24 16:26:01 +01:00
Laurent Cozic
da58d1f0d7 comments 2023-09-24 15:38:32 +01:00
Laurent Cozic
f34e08ad58 Tools: Trying to fix random CI failures (#8949) 2023-09-24 15:36:54 +01:00
Laurent Cozic
cd1e1cdf94 Desktop: Resolves #6736: Significantly improve performance when displaying large tables in Rich Text editor 2023-09-24 15:35:00 +01:00
Laurent Cozic
28384ddfd9 Desktop: Fixes #6514: Do no automatically set focus when switching layout to fix certain plugin issues 2023-09-24 14:41:42 +01:00
Laurent Cozic
061ec7902c Removed unneeded dependency 2023-09-24 13:29:09 +01:00
Laurent Cozic
e94b20d57a Doc: Fixed plan page layout 2023-09-24 13:20:22 +01:00
Laurent Cozic
04b52d7c1e fix test 2023-09-24 11:55:30 +01:00
renovate[bot]
63695637b9 Update dependency node-mocks-http to v1.13.0 (#8944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-23 18:07:09 +01:00
Laurent Cozic
a1821d607e Desktop: Fixes #5955: Changing the currently opened note from plugins or the data API does not refresh the note content 2023-09-23 17:50:24 +01:00
Laurent Cozic
cc459a68d5 lock file 2023-09-23 13:01:06 +01:00
Laurent Cozic
89b3c41d65 Doc: Add Joplin Cloud feature descriptions to Plans page 2023-09-23 12:56:18 +01:00
Gen Neko
62c4fd4b7d All: Translation: Update ja_JP.po (#8942) 2023-09-23 04:29:19 -04:00
github-actions[bot]
c24fe372fc @genneko has signed the CLA in laurent22/joplin#8942 2023-09-23 05:35:21 +00:00
renovate[bot]
fa0cd2e2a4 Update dependency @types/node to v18.17.14 (#8940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-23 03:47:08 +00:00
renovate[bot]
5078d975c8 Update dependency @types/react-redux to v7.1.26 (#8928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-23 02:02:51 +01:00
renovate[bot]
b742e36ea3 Update dependency @bam.tech/react-native-image-resizer to v3.0.7 (#8927)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-23 02:01:51 +01:00
renovate[bot]
2da5b6d848 Update dependency lint-staged to v13.3.0 (#8939)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-23 02:00:59 +01:00
Joplin Bot
662fc08cb1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-09-22 18:17:33 +00:00
renovate[bot]
1fe5fd17b8 Update dependency @types/node to v18.17.13 (#8936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-22 13:18:29 +00:00
Henry Heino
aff1bf501f Desktop: Resolves #8932: Fix pasting images into the beta editor (#8934) 2023-09-22 10:48:59 +01:00
renovate[bot]
2f3d207096 Update dependency npm-package-json-lint to v7 (#8938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-22 10:45:51 +01:00
Laurent Cozic
fc515313cf Merge branch 'release-2.12' into dev 2023-09-21 15:22:31 +01:00
Laurent Cozic
a0a4a81cc7 Desktop release v2.12.18 2023-09-21 15:16:07 +01:00
Laurent Cozic
01f37dfc32 Server: Fixed publishing logo 2023-09-21 11:30:15 +01:00
renovate[bot]
54966adbbb Update dependency react-native-safe-area-context to v4.7.2 (#8925)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-21 10:19:28 +00:00
Laurent Cozic
003d47fb72 lock file 2023-09-21 09:24:53 +01:00
Henry Heino
84c6de9b56 Desktop: Add new beta Markdown editor based on CodeMirror 6 (#8793) 2023-09-21 09:12:40 +01:00
Laurent Cozic
c3971ff226 Tools: Group buildTools on Renovate updates 2023-09-21 08:34:55 +01:00
renovate[bot]
874b39fe95 Update dependency react-native-modal-datetime-picker to v17.1.0 (#8888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-09-21 08:31:52 +01:00
renovate[bot]
41a78c9602 Update dependency eslint to v8.47.0 (#8924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-21 08:28:02 +01:00
Laurent Cozic
d43482a962 Server v2.13.1 2023-09-20 16:16:47 +01:00
Laurent Cozic
1b00445452 Server: Add Joplin Server and Joplin Cloud favicons 2023-09-20 16:14:51 +01:00
Henry Heino
fad4f6d1dd Desktop: Upgrade to Electron 25.8.1 (#8921) 2023-09-20 13:42:47 +01:00
renovate[bot]
58ab087d39 Update dependency glob to v10.3.4 (#8919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 07:49:57 +00:00
renovate[bot]
05e9cac111 Update dependency @types/zxcvbn to v4.4.2 (#8918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 03:26:19 +00:00
renovate[bot]
a8efad9887 Update dependency @types/uuid to v9.0.3 (#8917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 00:48:09 +00:00
renovate[bot]
2abc13b1c4 Update dependency @electron/remote to v2.0.11 (#8915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 22:51:31 +00:00
renovate[bot]
82bd65b622 Update dependency react-native-paper to v5.10.3 (#8913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 19:02:33 +00:00
Laurent Cozic
6962ba115e Server: Add function to get db default value 2023-09-19 14:52:59 +01:00
Laurent Cozic
6cdb1a714a Server: Set db default value in exported schema 2023-09-19 13:53:40 +01:00
Joplin Bot
8d4a1818c5 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-09-19 12:21:43 +00:00
renovate[bot]
9a2d8b19f4 Update dependency react-native-paper to v5.10.1 (#8908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-09-19 11:59:30 +01:00
Henry Heino
847213eb8c Mobile: Fixes #8798: Prevent accessibility tools from focusing the notes list when it's invisible (#8799) 2023-09-19 11:39:43 +01:00
CptMeetKat
728105f936 Desktop: Fixes #8643: Modified placeholder text on note title input field (#8850) 2023-09-19 11:37:20 +01:00
oj_lappi
e566f404a7 All: Resolves #8728: Bump mermaid version to 10.4.0 to support new chart types (#8890) 2023-09-19 11:31:17 +01:00
Henry Heino
bb5befc8bf Mobile: Fixes #8774: Hide the keyboard when showing the attach dialog (#8911) 2023-09-19 11:30:26 +01:00
Henry Heino
ec7f5aaf9b Desktop: Fixes #8898: Show a gear when a plugin doesn't specify an icon (#8909) 2023-09-19 11:29:19 +01:00
Laurent Cozic
06130a8e9a lock file and fixed linter 2023-09-19 10:13:13 +01:00
renovate[bot]
d9054ada91 Update dependency sass to v1.65.1 (#8910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 10:05:10 +01:00
Joplin Bot
324c9ad907 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-09-19 06:18:14 +00:00
Laurent Cozic
a04654a5a0 Server: Upgraded Formidable parsing library 2023-09-18 18:49:52 +01:00
Laurent Cozic
fa0740338d Desktop: Resolves ##5389: Add support for note list plugins (#8897) 2023-09-18 17:40:36 +01:00
Laurent Cozic
c3c5612dc5 renovate ignore 2023-09-18 17:32:22 +01:00
renovate[bot]
1b3c5db0c9 Update dependency react-native-exit-app to v2 (#8905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 11:25:58 +01:00
Milo Ivir
e94d684048 All: Translation: Update hr_HR.po (#8901) 2023-09-18 01:58:43 -04:00
renovate[bot]
331d101f94 Update dependency react-native-share to v9.2.4 (#8904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 00:53:40 +00:00
renovate[bot]
04ef13cdc5 Update dependency jquery to v3.7.1 (#8903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-17 21:12:11 +00:00
Laurent Cozic
b9cb9e42ff Desktop release v2.12.17 2023-09-14 22:06:59 +01:00
Laurent Cozic
7882237e24 All: Fixes #8699: Prevent application from being stuck when importing an invalid ENEX file 2023-09-14 22:06:25 +01:00
659 changed files with 176970 additions and 62088 deletions

View File

@@ -14,6 +14,7 @@ Modules/TinyMCE/IconPack/postinstall.js
Modules/TinyMCE/JoplinLists/
Modules/TinyMCE/langs/
node_modules/
packages/doc-builder/
packages/app-cli/build
packages/app-cli/build/
packages/app-cli/locales
@@ -38,6 +39,10 @@ packages/app-clipper/popup/config/webpack.config.js
packages/app-clipper/popup/node_modules
packages/app-clipper/popup/scripts/build.js
packages/app-desktop/build/
packages/app-desktop/test-results/
packages/app-desktop/playwright-report/
packages/app-desktop/playwright/.cache/
packages/app-desktop/integration-tests/test-profile/
packages/app-desktop/dist
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
@@ -47,7 +52,7 @@ packages/app-desktop/packageInfo.js
packages/app-desktop/services/electron-context-menu.js
packages/app-desktop/vendor/lib/
packages/app-mobile/android
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
packages/app-mobile/components/NoteEditor/**/*.bundle.js
packages/app-mobile/ios
packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
@@ -215,12 +220,11 @@ packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
packages/app-desktop/gui/Navigator.js
packages/app-desktop/gui/NoteContentPropertiesDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/setupVim.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
@@ -232,6 +236,14 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
@@ -365,6 +377,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -396,6 +415,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/components/ActionButton.js
packages/app-mobile/components/BackButtonDialogBox.js
@@ -407,28 +428,24 @@ packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
@@ -439,7 +456,6 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
@@ -461,6 +477,7 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
@@ -479,16 +496,52 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
packages/editor/CodeMirror/CodeMirrorControl.test.js
packages/editor/CodeMirror/CodeMirrorControl.js
packages/editor/CodeMirror/PluginLoader.js
packages/editor/CodeMirror/configFromSettings.js
packages/editor/CodeMirror/createEditor.test.js
packages/editor/CodeMirror/createEditor.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/editorCommands/swapLine.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
packages/editor/CodeMirror/markdown/markdownMathParser.js
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
packages/editor/CodeMirror/markdown/markdownReformatter.js
packages/editor/CodeMirror/markdown/syntaxHighlightingLanguages.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/util/isInSyntaxNode.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
packages/editor/types.js
packages/fork-htmlparser2/src/CollectingHandler.js
packages/fork-htmlparser2/src/FeedHandler.spec.js
packages/fork-htmlparser2/src/FeedHandler.js
@@ -539,6 +592,7 @@ packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
@@ -576,12 +630,14 @@ packages/lib/import-enex.js
packages/lib/initLib.js
packages/lib/locale.test.js
packages/lib/locale.js
packages/lib/makeDiscourseDebugUrl.js
packages/lib/markdownUtils.test.js
packages/lib/markdownUtils.js
packages/lib/markdownUtils2.test.js
packages/lib/markupLanguageUtils.js
packages/lib/migrations/42.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
@@ -607,6 +663,7 @@ packages/lib/models/SmartFilter.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/utils/isItemId.js
packages/lib/models/utils/itemCanBeEncrypted.js
packages/lib/models/utils/paginatedFeed.js
packages/lib/models/utils/paginationToSql.js
@@ -657,6 +714,10 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/migrations/42.js
packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
@@ -767,6 +828,7 @@ packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
packages/lib/services/rest/routes/revisions.js
packages/lib/services/rest/routes/search.js
packages/lib/services/rest/routes/tags.js
packages/lib/services/rest/utils/collectionToPaginatedResults.js
@@ -842,6 +904,7 @@ packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js
@@ -917,7 +980,9 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.js
@@ -954,12 +1019,15 @@ packages/tools/utils/loadSponsors.js
packages/tools/utils/translation.js
packages/tools/website/build.js
packages/tools/website/buildTranslations.js
packages/tools/website/processDocs.test.js
packages/tools/website/processDocs.js
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateNews.js
packages/tools/website/utils/applyTranslations.test.js
packages/tools/website/utils/applyTranslations.js
packages/tools/website/utils/convertLinksToLocale.test.js
packages/tools/website/utils/convertLinksToLocale.js
packages/tools/website/utils/frontMatter.test.js
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/news.js
packages/tools/website/utils/openGraph.test.js

View File

@@ -48,5 +48,5 @@ OS specifics:
<!--
Please attach a debug log. Issues without a debug log are likely to stall.
For information on how to collect a log file: https://joplinapp.org/debugging/
For information on how to collect a log file: https://joplinapp.org/help/apps/debugging/
-->

View File

@@ -86,7 +86,7 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
# Allocation failed - JavaScript heap out of memory
#
# https://stackoverflow.com/questions/38558989
export NODE_OPTIONS="--max-old-space-size=4096"
export NODE_OPTIONS="--max-old-space-size=32768"
yarn run test-ci
testResult=$?
if [ $testResult -ne 0 ]; then
@@ -94,6 +94,17 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
fi
fi
# =============================================================================
# Check that the website builder can run without errors
# =============================================================================
if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Running website builder..."
node packages/tools/website/processDocs.js --env dev
fi
fi
# =============================================================================
# Run linter for pull requests only. We also don't want this to make the desktop
# release randomly fail.

View File

@@ -13,7 +13,7 @@ jobs:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.3.0
uses: contributor-assistant/github-action@v2.3.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret

View File

@@ -3,7 +3,9 @@ on: [push, pull_request]
jobs:
pre_job:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
runs-on: ubuntu-20.04
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
@@ -19,7 +21,9 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-2019]
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-latest, ubuntu-20.04, windows-2019]
steps:
# Trying to fix random networking issues on Windows
@@ -51,6 +55,9 @@ jobs:
sudo apt-get install -y libsecret-1-dev
sudo apt-get install -y translate-toolkit
sudo apt-get install -y rsync
# Provides a virtual display on Linux. Used for Playwright integration
# testing.
sudo apt-get install -y xvfb
- name: Install Docker Engine
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
@@ -139,7 +146,9 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [ubuntu-20.04]
steps:
- name: Install Docker Engine

106
.gitignore vendored
View File

@@ -51,6 +51,7 @@ lerna-debug.log
.env
docs/**/*.mustache
.idea
/readme/i18n
# Yarn stuff
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
@@ -201,12 +202,11 @@ packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
packages/app-desktop/gui/Navigator.js
packages/app-desktop/gui/NoteContentPropertiesDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/setupVim.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
@@ -218,6 +218,14 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
@@ -351,6 +359,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -382,6 +397,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/components/ActionButton.js
packages/app-mobile/components/BackButtonDialogBox.js
@@ -393,28 +410,24 @@ packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
@@ -425,7 +438,6 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
@@ -447,6 +459,7 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
@@ -465,16 +478,52 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
packages/editor/CodeMirror/CodeMirrorControl.test.js
packages/editor/CodeMirror/CodeMirrorControl.js
packages/editor/CodeMirror/PluginLoader.js
packages/editor/CodeMirror/configFromSettings.js
packages/editor/CodeMirror/createEditor.test.js
packages/editor/CodeMirror/createEditor.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/editorCommands/swapLine.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
packages/editor/CodeMirror/markdown/markdownMathParser.js
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
packages/editor/CodeMirror/markdown/markdownReformatter.js
packages/editor/CodeMirror/markdown/syntaxHighlightingLanguages.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/util/isInSyntaxNode.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
packages/editor/types.js
packages/fork-htmlparser2/src/CollectingHandler.js
packages/fork-htmlparser2/src/FeedHandler.spec.js
packages/fork-htmlparser2/src/FeedHandler.js
@@ -525,6 +574,7 @@ packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
@@ -562,12 +612,14 @@ packages/lib/import-enex.js
packages/lib/initLib.js
packages/lib/locale.test.js
packages/lib/locale.js
packages/lib/makeDiscourseDebugUrl.js
packages/lib/markdownUtils.test.js
packages/lib/markdownUtils.js
packages/lib/markdownUtils2.test.js
packages/lib/markupLanguageUtils.js
packages/lib/migrations/42.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
@@ -593,6 +645,7 @@ packages/lib/models/SmartFilter.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/utils/isItemId.js
packages/lib/models/utils/itemCanBeEncrypted.js
packages/lib/models/utils/paginatedFeed.js
packages/lib/models/utils/paginationToSql.js
@@ -643,6 +696,10 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/migrations/42.js
packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
@@ -753,6 +810,7 @@ packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
packages/lib/services/rest/routes/revisions.js
packages/lib/services/rest/routes/search.js
packages/lib/services/rest/routes/tags.js
packages/lib/services/rest/utils/collectionToPaginatedResults.js
@@ -828,6 +886,7 @@ packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js
@@ -903,7 +962,9 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.test.js
packages/tools/buildServerDocker.js
@@ -940,12 +1001,15 @@ packages/tools/utils/loadSponsors.js
packages/tools/utils/translation.js
packages/tools/website/build.js
packages/tools/website/buildTranslations.js
packages/tools/website/processDocs.test.js
packages/tools/website/processDocs.js
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateNews.js
packages/tools/website/utils/applyTranslations.test.js
packages/tools/website/utils/applyTranslations.js
packages/tools/website/utils/convertLinksToLocale.test.js
packages/tools/website/utils/convertLinksToLocale.js
packages/tools/website/utils/frontMatter.test.js
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/news.js
packages/tools/website/utils/openGraph.test.js

View File

@@ -1,2 +1,3 @@
packages/app-clipper/popup/
packages/app-cli/tests/support/plugins/
packages/app-cli/tests/support/plugins/
packages/doc-builder/

View File

@@ -5,6 +5,7 @@
"exceptions": [
"@joplin/lib",
"@joplin/renderer",
"@joplin/editor",
"@joplin/pdf-viewer",
"@joplin/fork-htmlparser2",
"@joplin/fork-sax",

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.3.cjs
yarnPath: .yarn/releases/yarn-3.6.4.cjs
logFilters:

View File

@@ -0,0 +1,15 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_52_28)">
<rect width="84" height="84" fill="url(#paint0_linear_52_28)"/>
<path d="M66 48.5424C66 51.4195 64.8173 54.0531 62.9108 55.9371C61.0108 57.8274 58.3546 59 55.4529 59H49.1324V53.5533H55.4529C56.8553 53.5533 58.1026 52.9959 59.0268 52.086C59.9445 51.1696 60.5067 49.9393 60.5067 48.5424C60.5067 47.1519 59.9445 45.9152 59.0268 44.9989C58.1026 44.089 56.8618 43.5315 55.4529 43.5315H50.1083L50.4056 40.539C50.4379 40.2314 50.4508 39.9303 50.4508 39.6163C50.4508 37.0724 49.4168 34.7976 47.7429 33.1316C46.0626 31.4655 43.7684 30.4467 41.2027 30.4467C38.637 30.4467 36.3428 31.4719 34.6625 33.1316C32.9822 34.7976 31.9546 37.0724 31.9546 39.6163C31.9546 39.9239 31.9675 40.2314 31.9999 40.539L32.2971 43.5315H27.5535C26.1511 43.5315 24.9039 44.089 23.9797 44.9989C23.062 45.9152 22.4997 47.1455 22.4997 48.5424C22.4997 49.9329 23.062 51.1696 23.9797 52.086C24.9039 52.9959 26.1447 53.5533 27.5535 53.5533H35.2118C36.6077 53.3291 37.9003 52.6755 38.9278 51.663C40.2139 50.3815 40.9248 48.6834 40.9248 46.8828V41.0837H35.3605V38.9819C35.3605 37.1941 36.8275 35.7331 38.637 35.7331H46.3211V46.8828C46.3211 50.0739 45.0222 53.1945 42.7473 55.4437C40.7439 57.4365 38.0619 58.6732 35.2441 58.9359L34.5332 59H27.5471C24.6453 59 21.9892 57.8274 20.0892 55.9371C18.1827 54.0531 17 51.4195 17 48.5424C17 45.6653 18.1827 43.0317 20.0892 41.1478C21.763 39.4817 24.0249 38.3732 26.526 38.1361C26.875 34.6951 28.4389 31.5937 30.772 29.2805C33.4346 26.6404 37.1377 24.9936 41.1963 25C45.2613 25 48.9644 26.6404 51.6205 29.2805C53.9471 31.5809 55.5046 34.6759 55.8601 38.1041C58.6002 38.213 61.0884 39.3472 62.8979 41.1478C64.8173 43.0317 66 45.6653 66 48.5424Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_52_28" x1="3" y1="4" x2="78" y2="79" gradientUnits="userSpaceOnUse">
<stop offset="0.14" stop-color="#3873DB"/>
<stop offset="0.946667" stop-color="#163467"/>
</linearGradient>
<clipPath id="clip0_52_28">
<rect width="84" height="84" rx="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="84"
height="84"
viewBox="0 0 84 84"
fill="none"
version="1.1"
id="svg938"
sodipodi:docname="JoplinCloudIcon copie.svg"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview940"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.5021331"
inkscape:cx="-6.1391156"
inkscape:cy="29.838957"
inkscape:window-width="1310"
inkscape:window-height="969"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="g926" />
<g
clip-path="url(#clip0_52_28)"
id="g926">
<rect
width="84"
height="84"
fill="url(#paint0_linear_52_28)"
id="rect922" />
<path
d="m 73.705932,49.824628 c 0,3.73216 -1.534192,7.148453 -4.007294,9.592368 -2.464671,2.452087 -5.91028,3.973178 -9.674351,3.973178 h -8.198921 v -7.065432 h 8.198921 c 1.819186,0 3.437178,-0.723056 4.636045,-1.903374 1.190436,-1.188749 1.919719,-2.784688 1.919719,-4.59674 0,-1.803749 -0.729283,-3.40799 -1.919719,-4.59661 -1.198867,-1.180318 -2.808427,-1.903504 -4.636045,-1.903504 h -6.932988 l 0.385656,-3.881856 c 0.0419,-0.399017 0.05863,-0.789603 0.05863,-1.196922 0,-3.299934 -1.341299,-6.250793 -3.512674,-8.41192 -2.179677,-2.161256 -5.155701,-3.482839 -8.483914,-3.482839 -3.328214,0 -6.304238,1.329885 -8.483915,3.482839 -2.179677,2.161127 -3.512674,5.111986 -3.512674,8.41192 0,0.399017 0.01673,0.797905 0.05876,1.196922 l 0.385526,3.881856 H 23.83332 c -1.819186,0 -3.437048,0.723186 -4.635915,1.903504 -1.190436,1.18862 -1.919849,2.784559 -1.919849,4.59661 0,1.80375 0.729413,3.407991 1.919849,4.59674 1.198867,1.180318 2.808427,1.903374 4.635915,1.903374 h 9.934308 c 1.810755,-0.290831 3.487509,-1.138677 4.820377,-2.452087 1.668322,-1.662356 2.590498,-3.865122 2.590498,-6.200852 V 40.14924 h -7.217982 v -2.726444 c 0,-2.319125 1.902985,-4.214327 4.250259,-4.214327 h 9.967776 v 14.463334 c 0,4.139479 -1.684926,8.187506 -4.635915,11.105157 -2.598801,2.58505 -6.077877,4.189291 -9.733113,4.530064 l -0.922176,0.08315 h -9.062334 c -3.7642,0 -7.20968,-1.521091 -9.67435,-3.973178 -2.473097,-2.443915 -4.007289,-5.860208 -4.007289,-9.592368 0,-3.732159 1.534192,-7.148452 4.007295,-9.592238 2.171245,-2.161256 5.10537,-3.599197 8.349784,-3.906762 0.452721,-4.463648 2.481404,-8.486768 5.50789,-11.487439 3.453911,-3.424725 8.257554,-5.560946 13.522349,-5.552643 5.273098,0 10.076741,2.127918 13.52222,5.552643 3.018054,2.984067 5.038435,6.998885 5.499588,11.445929 3.554444,0.141265 6.782124,1.612543 9.129399,3.948272 2.489836,2.443786 4.024028,5.860079 4.024028,9.592238 z"
fill="#ffffff"
id="path924"
style="stroke-width:1.29719" />
</g>
<defs
id="defs936">
<linearGradient
id="paint0_linear_52_28"
x1="3"
y1="4"
x2="78"
y2="79"
gradientUnits="userSpaceOnUse">
<stop
offset="0.14"
stop-color="#3873DB"
id="stop928" />
<stop
offset="0.946667"
stop-color="#163467"
id="stop930" />
</linearGradient>
<clipPath
id="clip0_52_28">
<rect
width="84"
height="84"
rx="20"
fill="white"
id="rect933" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,16 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_309_806)">
<rect width="84" height="84" fill="url(#paint0_linear_309_806)"/>
<path d="M42.5797 45.6916H33.6235V36.276C33.6235 33.3538 34.7629 30.6033 36.8396 28.5204C38.4998 26.8725 40.5826 25.8127 42.8431 25.4513V16.7033C38.2854 17.1138 33.9359 19.1354 30.6891 22.3822C27.0258 26.0333 24.9307 31.0995 24.9307 36.2821V54.3845H37.3052C40.2151 54.3845 42.5797 52.0198 42.5797 49.11V45.6916Z" fill="#1883E6"/>
<path d="M42.4204 35.89V39.3022H51.3767V48.7179C51.3767 51.64 50.2372 54.3906 48.1605 56.4735C46.5003 58.1214 44.4175 59.1812 42.157 59.5426V68.2906C46.7147 67.8801 51.0642 65.8585 54.311 62.6117C57.9744 58.9606 60.0695 53.8944 60.0695 48.7118V30.6155H47.6949C44.7851 30.6155 42.4204 32.9863 42.4204 35.89Z" fill="#1883E6"/>
</g>
<defs>
<linearGradient id="paint0_linear_309_806" x1="8.5" y1="3" x2="76.5" y2="81.5" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#B6D3F9"/>
</linearGradient>
<clipPath id="clip0_309_806">
<rect width="84" height="84" rx="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -667,6 +667,30 @@ footer .bottom-links-row p {
color: #0557ba;
}
.joplin-cloud-feature-list .feature-description {
max-width: 600px;
font-size: .8em;
color: #555555;
margin-top: 5px;
}
.joplin-cloud-feature-list .feature-title {
text-decoration: none;
color: #000000;
margin-left: 10px;
border: 2px solid black;
display: inline-block;
width: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100px;
height: 20px;
font-weight: bold;
font-size: 0.8em;
opacity: 0.5;
}
/*****************************************************************
WHAT'S NEW PAGE
*****************************************************************/

View File

@@ -77,7 +77,7 @@ function setupDownloadPage() {
} else if (os === 'linux') {
// If it's Linux, the user should probably install it using the
// install script so we redirect to the install section
window.location = 'https://joplinapp.org/help/#desktop-applications';
window.location = 'https://joplinapp.org/help/install';
} else {
// Otherwise, start the download
const downloadLink = downloadLinks[os];

View File

@@ -1,30 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Wed, 06 Sep 2023 12:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Wed, 06 Sep 2023 12:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.12]]></title><description><![CDATA[<h1>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h1>
<h2>Support for Apple Silicon<a name="support-for-apple-silicon" href="#support-for-apple-silicon" class="heading-anchor">🔗</a></h2>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 23 Oct 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><item><title><![CDATA[Working in the shadows with white-hat hackers]]></title><description><![CDATA[<p>The majority of Joplin's development is carried out in the public domain. This includes the discussion of issues on GitHub, as well as the submission of pull requests and related discussions. The transparency of these processes allows for collaborative problem-solving and shared insights.</p>
<p>However, there is one aspect that operates behind closed doors, and for good reason: addressing cybersecurity vulnerabilities. It is imperative that these issues remain undisclosed until they have been resolved. Once a solution is implemented, it is usually accompanied by discreet commits and a message in the changelog to signify the progress made.</p>
<p>Typically, the process begins with an email from a security researcher. They provide valuable insights, such as a specially crafted note that triggers a bug, or an API call, along with an explanation of how the application's security can be circumvented. We examine the vulnerability, create a fix, and create automated test units to prevent any accidental reintroduction of the vulnerability in future code updates. An example of such a commit is: <a href="https://github.com/laurent22/joplin/commit/9e90d9016daf79b5414646a93fd369aedb035071">9e90d9016daf79b5414646a93fd369aedb035071</a></p>
<p>We then share our fix with the researcher for validation. Additionally, we often apply the fix to previous versions of Joplin, depending on the severity of the vulnerability.</p>
<p>The contribution of security researchers in this regard is immeasurable. They employ their ingenuity to identify inventive methods of bypassing existing security measures and often discover subtle flaws in the code that might otherwise go unnoticed.</p>
<p>We would like to express our sincere gratitude to the security researchers who have assisted us throughout the years in identifying and rectifying security vulnerabilities!</p>
<ul>
<li><a href="https://github.com/a1ise">@Alise</a></li>
<li>@hexodotsh</li>
<li><a href="https://github.com/ly1g3">@ly1g3</a></li>
<li><a href="https://twitter.com/maple3142">@maple3142</a></li>
<li>Ademar Nowasky Junior</li>
<li><a href="mailto:ben@mayhem.sg">Benjamin Harris</a></li>
<li><a href="https://github.com/JavierOlmedo">Javier Olmedo</a></li>
<li><a href="https://twitter.com/newfolderj">Jubair Rehman Yousafzai</a></li>
<li>lin@UCCU Hacker</li>
<li><a href="https://github.com/personalizedrefrigerator">personalizedrefrigerator</a></li>
<li><a href="https://twitter.com/fhlipZero">Phil Holbrook</a></li>
<li><a href="https://ryotak.net/">RyotaK</a></li>
<li><a href="https://twitter.com/YNizry">Yaniv Nizry</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20231023-white-hat-hackers</link><guid isPermaLink="false">20231023-white-hat-hackers</guid><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><twitter-text>Working in the shadows with white-hat hackers</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.12]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
<h3>Support for Apple Silicon<a name="support-for-apple-silicon" href="#support-for-apple-silicon" class="heading-anchor">🔗</a></h3>
<p>A new release is now available for Apple Silicon, which provides improve performances on this architecture.</p>
<h2>Rich Text editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h2>
<h3>Rich Text editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
<p>In this release, we've undertaken numerous enhancements and addressed various bugs in the Rich Text editor. Notably, we've introduced support for plugin toolbar icons. Additionally, we've refined the editor's ability to manage text that's copied from applications like Word, Office, and LibreOffice, thereby enhancing cross-application compatibility. Among the minor yet impactful improvements, we've fine-tuned the handling of newlines and paragraphs—a highly requested feature. Another notable update is the improved automatic switching between light and dark modes.</p>
<p>Altogether, this release encompasses around 12 significant changes for the Rich Text editor, and rest assured, there's more to come in the future!</p>
<h2>Share permissions<a name="share-permissions" href="#share-permissions" class="heading-anchor">🔗</a></h2>
<h3>Share permissions<a name="share-permissions" href="#share-permissions" class="heading-anchor">🔗</a></h3>
<p>Using Joplin Cloud Pro and Teams, you now have the ability to customize the read and write permissions for the notebooks you share. You have the option to grant other users permission to edit the notes or share them as read-only. This ensures that you can confidently share a notebook without worrying about unintentional modifications by your friends or colleagues.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
<h2>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h2>
<h3>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h3>
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named &quot;Inbox.&quot;</p>
<p>More information in the <a href="https://joplinapp.org/email%5C_to%5C_note/">Email to Note documentation</a>.</p>
<h2>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h2>
<p>More information in the <a href="https://joplinapp.org/help/apps/email_to_note">Email to Note documentation</a>.</p>
<h3>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h3>
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>
<h2>Rotating log<a name="rotating-log" href="#rotating-log" class="heading-anchor">🔗</a></h2>
<h3>Rotating log<a name="rotating-log" href="#rotating-log" class="heading-anchor">🔗</a></h3>
<p>Up until now, the logs could grow to become very large, and if you wanted to shrink them down you would have to manually delete them. With this new release, we now automatically handle logs by rotating them - once the log becomes large enough it is moved to an archive. And once the archive is older than a number of days, it is deleted.</p>
<h2>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h2>
<h3>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h3>
<p>Like the desktop app, the mobile apps benefits from the Share Permissions update, as well as the Email to Note feature for Joplin Cloud. There are also plenty of other improvements - all in all there's 23 bug fixes, enhancements and security fixes in this release.</p>
<h1>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h1>
<h2>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h2>
<p>This is just an overview of the main features. The full changelog is available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/changelog">https://joplinapp.org/changelog</a></li>
<li>Android: <a href="https://joplinapp.org/changelog_android">https://joplinapp.org/changelog_android</a></li>
<li>iOS: <a href="https://joplinapp.org/changelog_ios">https://joplinapp.org/changelog_ios</a></li>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android">https://joplinapp.org/help/about/changelog/android</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios">https://joplinapp.org/help/about/changelog/ios</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20230508-release-2-12/</link><guid isPermaLink="false">20230508-release-2-12</guid><pubDate>Wed, 06 Sep 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.12</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.11]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
]]></description><link>https://joplinapp.org/news/20230508-release-2-12</link><guid isPermaLink="false">20230508-release-2-12</guid><pubDate>Wed, 06 Sep 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.12</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.11]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
<h3>Add support for plugin user data<a name="add-support-for-plugin-user-data" href="#add-support-for-plugin-user-data" class="heading-anchor">🔗</a></h3>
<p>Developers of plugins now have the ability to associate additional information with notes, notebooks, and tags. This data is then synchronized across multiple devices. For instance, it is possible to envision attaching OCR data to an image or incorporating plugin-specific parameters to a note, which can subsequently be synchronized across all devices.</p>
<h3>Improved end-to-end encryption<a name="improved-end-to-end-encryption" href="#improved-end-to-end-encryption" class="heading-anchor">🔗</a></h3>
@@ -50,19 +71,19 @@
<h2>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h2>
<p>This is just an overview of the main features. The full changelog is available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/changelog">https://joplinapp.org/changelog</a></li>
<li>Android: <a href="https://joplinapp.org/changelog_android">https://joplinapp.org/changelog_android</a></li>
<li>iOS: <a href="https://joplinapp.org/changelog_ios">https://joplinapp.org/changelog_ios</a></li>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android">https://joplinapp.org/help/about/changelog/android</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios">https://joplinapp.org/help/about/changelog/ios</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20230508-release-2-11/</link><guid isPermaLink="false">20230508-release-2-11</guid><pubDate>Fri, 14 Jul 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.11</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
<h1>New design for &quot;New note&quot; and &quot;New to-do&quot; buttons<a name="new-design-for-new-note-and-new-to-do-buttons" href="#new-design-for-new-note-and-new-to-do-buttons" class="heading-anchor">🔗</a></h1>
]]></description><link>https://joplinapp.org/news/20230508-release-2-11</link><guid isPermaLink="false">20230508-release-2-11</guid><pubDate>Fri, 14 Jul 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.11</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
<h2>New design for &quot;New note&quot; and &quot;New to-do&quot; buttons<a name="new-design-for-new-note-and-new-to-do-buttons" href="#new-design-for-new-note-and-new-to-do-buttons" class="heading-anchor">🔗</a></h2>
<p>We're excited to announce that we've made it even easier to create new notes and to-do lists with new designs for the &quot;New note&quot; and &quot;New to-do&quot; buttons.</p>
<p>If there is enough space, the button labels will be shown in full:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-1.png" alt=""></p>
<p>While for those who prefer a more narrow note list, only the button icons will be shown:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-2.png" alt=""></p>
<p>It's a small improvement, but we're confident it will make the app even more intuitive for new users.</p>
<h1>Fixes and improvements<a name="fixes-and-improvements" href="#fixes-and-improvements" class="heading-anchor">🔗</a></h1>
<h2>Fixes and improvements<a name="fixes-and-improvements" href="#fixes-and-improvements" class="heading-anchor">🔗</a></h2>
<p>This version includes 30 bug fixes and 16 general improvements. Let's dive into some of the highlights:</p>
<ul>
<li>
@@ -79,12 +100,12 @@
</li>
</ul>
<p>Last but not least, we have modernised both the desktop and mobile application modules, just <a href="https://joplinapp.org/news/20221115-renovate/">as we previously announced</a>. Although these changes may not be visible to you, they required a lot of work! But the result is that our applications are now more stable and secure, and it will be easier to maintain them in the long run. We're using a tool called <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate</a>, which will automatically propose package updates that we will review. In total, we've updated a whopping 633 packages so far!</p>
<h1>Android app is available in the Play Store<a name="android-app-is-available-in-the-play-store" href="#android-app-is-available-in-the-play-store" class="heading-anchor">🔗</a></h1>
<h2>Android app is available in the Play Store<a name="android-app-is-available-in-the-play-store" href="#android-app-is-available-in-the-play-store" class="heading-anchor">🔗</a></h2>
<p>Our latest version, 2.10, is now back in the Play Store and ready for download! Although we had to skip 2.9 due to some of Google's requirements, we worked hard to ensure that our app complies with their standards, and we are excited to announce that we are back and better than ever! Our iOS version is also available, so you can continue to enjoy the app regardless of the platform you use.</p>
<h2>Biometrics support<a name="biometrics-support" href="#biometrics-support" class="heading-anchor">🔗</a></h2>
<h3>Biometrics support<a name="biometrics-support" href="#biometrics-support" class="heading-anchor">🔗</a></h3>
<p>To make your experience even more secure, our Android and iOS apps now support biometric unlock! With just a quick scan of your fingerprint or Face ID, you can unlock your app in no time. To enable this beta feature, just head over to the settings and click on &quot;Use biometrics to secure access to the app&quot;.</p>
<p>We've tested this feature thoroughly during prerelease, and have already fixed all known issues. However we still consider it as a beta feature for now, so if you run into any issues please let us know.</p>
<h2>Support for multiple profile<a name="support-for-multiple-profile" href="#support-for-multiple-profile" class="heading-anchor">🔗</a></h2>
<h3>Support for multiple profile<a name="support-for-multiple-profile" href="#support-for-multiple-profile" class="heading-anchor">🔗</a></h3>
<p>We're thrilled to announce that multiple profiles are now supported in our mobile app! To create a new profile, simply go to the Configuration screen and click on &quot;Manage profiles&quot; under the Tools section.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-1.png" alt=""></p>
<p>From there, you can easily add or remove profiles as needed.</p>
@@ -92,14 +113,14 @@
<p>Once multiple profiles are setup, you will see a new option in the sidebar to quickly toggle from one profile to another:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-3.png" alt=""></p>
<p>Once you've set up multiple profiles, you can easily toggle between them using the new option in the sidebar. This feature is perfect for separating your personal and work notes into independent collections.</p>
<h2>Support for realtime search<a name="support-for-realtime-search" href="#support-for-realtime-search" class="heading-anchor">🔗</a></h2>
<h3>Support for realtime search<a name="support-for-realtime-search" href="#support-for-realtime-search" class="heading-anchor">🔗</a></h3>
<p>Our mobile app now has an improved search function that performs text searches in real time! No more waiting for the search results to load, they'll appear instantly as you type.</p>
<h2>Improved filesystem sync performance<a name="improved-filesystem-sync-performance" href="#improved-filesystem-sync-performance" class="heading-anchor">🔗</a></h2>
<h3>Improved filesystem sync performance<a name="improved-filesystem-sync-performance" href="#improved-filesystem-sync-performance" class="heading-anchor">🔗</a></h3>
<p>Thanks to the hard work of jd1378, the sync no longer freezes during filesystem synchronisation. We know how frustrating that can be, and we're thrilled to have solved this issue. Getting filesystem sync to work on Android is never easy due to the restrictions put in place by Google, especially since they frequently change, but we're committed to delivering the best possible experience for our Android users.</p>
]]></description><link>https://joplinapp.org/news/20230508-release-2-10/</link><guid isPermaLink="false">20230508-release-2-10</guid><pubDate>Wed, 10 May 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.10</twitter-text></item><item><title><![CDATA[Joplin will participate in JdLL 2023!]]></title><description><![CDATA[<p>On 1 and 2 April 2023, we will have a stand for Joplin at the <a href="https://www.jdll.org/">Journées du Logiciel Libre</a> in Lyon, France. The JdLL has been taking place in Lyon for 24 years and is a popular open source conference in France. We had a stand in 2020 and 2021 but that was cancelled due to Covid, so this year is a first for Joplin!</p>
]]></description><link>https://joplinapp.org/news/20230508-release-2-10</link><guid isPermaLink="false">20230508-release-2-10</guid><pubDate>Wed, 10 May 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.10</twitter-text></item><item><title><![CDATA[Joplin will participate in JdLL 2023!]]></title><description><![CDATA[<p>On 1 and 2 April 2023, we will have a stand for Joplin at the <a href="https://www.jdll.org/">Journées du Logiciel Libre</a> in Lyon, France. The JdLL has been taking place in Lyon for 24 years and is a popular open source conference in France. We had a stand in 2020 and 2021 but that was cancelled due to Covid, so this year is a first for Joplin!</p>
<p>Admission is free, so don't hesitate to come and meet us, exchange ideas and learn more about Joplin!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230202-jdll.jpg" alt="Joplin at JdLL 2023"></p>
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023/</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
<p>Thankfully GitHub provides an alternative access: the raw logs. This is much better because they will open as plain text, without any styling or JS magic, which means you can use the browser native search and it will be fast.</p>
<p>But now the problem is that raw logs look like this:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log.png" alt="Raw log without extension"></p>
@@ -110,7 +131,7 @@
<p>The extension is open source, with the code available here: <a href="https://github.com/laurent22/github-actions-logs-extension">https://github.com/laurent22/github-actions-logs-extension</a></p>
<p>And to install it, follow this link:</p>
<p><a href="https://chrome.google.com/webstore/detail/github-action-raw-log-vie/lgejlnoopmcdglhfjblaeldbcfnmjddf"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-extension-get-it-now.png" alt="Download GitHub Action Raw Log Viewer extension"></a></p>
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer/</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the &quot;GitHub Action Raw Log Viewer&quot; extension for Chrome</twitter-text></item><item><title><![CDATA[Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)]]></title><description><![CDATA[<p>As was <a href="https://discourse.joplinapp.org/t/rfc-switch-to-agpl-license-for-joplin-server/16529">discussed last year</a>, Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0) for the desktop, mobile and CLI applications, as well as the web clipper.</p>
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the &quot;GitHub Action Raw Log Viewer&quot; extension for Chrome</twitter-text></item><item><title><![CDATA[Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)]]></title><description><![CDATA[<p>As was <a href="https://discourse.joplinapp.org/t/rfc-switch-to-agpl-license-for-joplin-server/16529">discussed last year</a>, Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0) for the desktop, mobile and CLI applications, as well as the web clipper.</p>
<p>Any open source or commercial fork of Joplin will have to license any changes they make under AGPL, and share these changes back with the community. This is the main reason we switch to this license. It allows us to continue releasing the project as open source while ensuring that those who benefit commercially (or not) from it share back their changes.</p>
<h2>What is the GPL license?<a name="what-is-the-gpl-license" href="#what-is-the-gpl-license" class="heading-anchor">🔗</a></h2>
<p>The AGPL license is based on the GPL license. This is what tldr;Legal has to say about the GPL license:</p>
@@ -129,7 +150,7 @@
<p>This is a bit of an extra constraint but it is hard to avoid. Contributor License Agreements are very common for GPL or AGPL projects. For example Apache, Canonical or Python all require their contributors to sign a CLA.</p>
<h2>Questions?<a name="questions" href="#questions" class="heading-anchor">🔗</a></h2>
<p>If you have any questions please let us know. Overall we believe this is a positive improvements for Joplin as it means any work derives from it will also benefit the project.</p>
]]></description><link>https://joplinapp.org/news/20221221-agpl/</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.9]]></title><description><![CDATA[<h2>Proxy support<a name="proxy-support" href="#proxy-support" class="heading-anchor">🔗</a></h2>
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.9]]></title><description><![CDATA[<h2>Proxy support<a name="proxy-support" href="#proxy-support" class="heading-anchor">🔗</a></h2>
<p>Both the desktop and mobile application now support proxies thanks to the work of Jason Williams. This will allow you to use the apps in particular when you are behind a company proxy.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-proxy-support.png" alt=""></p>
<h2>New PDF viewer<a name="new-pdf-viewer" href="#new-pdf-viewer" class="heading-anchor">🔗</a></h2>
@@ -160,26 +181,26 @@
<p>As always, a lot of work went into the Android and iOS app too, which include 37 improvements, bug fixes, and security fixes.</p>
<p>See here for the changelogs:</p>
<ul>
<li><a href="https://joplinapp.org/changelog/">Desktop app changelog</a></li>
<li><a href="https://joplinapp.org/changelog_android/">Android app changelog</a></li>
<li><a href="https://joplinapp.org/help/about/changelog/desktop">Desktop app changelog</a></li>
<li><a href="https://joplinapp.org/help/about/changelog/android/">Android app changelog</a></li>
</ul>
<h2>About the Android version<a name="about-the-android-version" href="#about-the-android-version" class="heading-anchor">🔗</a></h2>
<p>Unfortunately we cannot publish the Android version because it is based on a framework version that Google does not accept. To upgrade the app a lot of changes are needed and another round of pre-releases, and therefore there will not be a 2.9 version for Google Play. You may however download the official APK directly from there: <a href="https://github.com/laurent22/joplin-android/releases/tag/android-v2.9.8">Android 2.9 Official Release</a></p>
<p>This is the reality of app stores in general - small developers being imposed never ending new requirements by all-powerful companies, and by the time a version is finally ready we can't even publish it because yet more requirements are in place.</p>
<p>For the record the current 2.9 app works perfectly fine. It targets Android 11, which is only 2 years old and is still supported (and installed on millions of phones). Google requires us to target Android 12 which only came out last year.</p>
]]></description><link>https://joplinapp.org/news/20221216-release-2-9/</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.9</twitter-text></item><item><title><![CDATA[Joplin is hiring!]]></title><description><![CDATA[<p>Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.</p>
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.9</twitter-text></item><item><title><![CDATA[Joplin is hiring!]]></title><description><![CDATA[<p>Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.</p>
<p>We are looking to hire two JavaScript software developers to work on the desktop, mobile, and server applications. All those are built using modern technologies, including React, React Native and Electron with a strong focus on test units.</p>
<p>You need to demonstrate some experience with at least some of these technologies, and willing to learn more and touch various different projects.</p>
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
<p>No agencies please.</p>
]]></description><link>https://joplinapp.org/news/20221209-job/</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item><item><title><![CDATA[Modernising and securing Joplin, one package at a time]]></title><description><![CDATA[<p>If you watch the <a href="https://github.com/laurent22/joplin">Joplin source code repository</a>, you may have noticed a lot of Renovate pull requests lately. This <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate tool</a> is a way to manage dependencies - it automatically finds what needs to be updated, then upgrade it to the latest version, and create a pull request. If all tests pass, we can then merge this pull request. So far we have merged 267 of these pull requests.</p>
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item><item><title><![CDATA[Modernising and securing Joplin, one package at a time]]></title><description><![CDATA[<p>If you watch the <a href="https://github.com/laurent22/joplin">Joplin source code repository</a>, you may have noticed a lot of Renovate pull requests lately. This <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate tool</a> is a way to manage dependencies - it automatically finds what needs to be updated, then upgrade it to the latest version, and create a pull request. If all tests pass, we can then merge this pull request. So far we have merged 267 of these pull requests.</p>
<p>Updating Joplin packages was long due. It is necessary so that we don't fall behind and end up using unsupported or deprecated packages. We also benefit from bug fixes and performance improvements. It is also important in terms of security, since recent package versions usually include various security fixes.</p>
<p>We used to rely on a tool called &quot;npm audit&quot; to do this, however it no longer works on the Joplin codebase, and it was always risky to use it since it would update multiple packages in one command - so if something went wrong it was difficult to find the culprit.</p>
<p>Renovate on the other hand upgrades packages one at a time, and run our test units to ensure everything is still working as expected. It also upgrades multiple instances of the same package across the monorepo, which is convenient to keep our code consistent. It also has a number of options to make our life easier, such as the ability to automatically merge a pull request for patch releases since this is usually safe (when a package is, for example upgraded from 1.0.1 to 1.0.3).</p>
<p>Although Renovate automates the package upgrades it doesn't mean all upgrades are straightforward - our tests won't catch all issues, so the apps might end up being broken or cannot be compiled anymore. So there's manual work involved to get everything working after certain upgrades - for the most part this has been done and the apps appear to be stable so far.</p>
<p>This will however be an important part of pre-release 2.10 (or should it be 3.0?) - we hope that everything works but we may need your support to try this version and report any glitch you may have found. As always pre-release regressions have the highest priority so we aim to fix them as quickly as possible.</p>
]]></description><link>https://joplinapp.org/news/20221115-renovate/</link><guid isPermaLink="false">20221115-renovate</guid><pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate><twitter-text>Modernising and securing Joplin, one package at a time</twitter-text></item><item><title><![CDATA[Joplin Cloud is now part of the Joplin company]]></title><description><![CDATA[<p>As some of you may know Joplin Cloud so far has been operating under my own single-person limited company in the UK. This was mostly for convenience since it meant I could get things going quickly without having to setup a special structure for it.</p>
]]></description><link>https://joplinapp.org/news/20221115-renovate</link><guid isPermaLink="false">20221115-renovate</guid><pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate><twitter-text>Modernising and securing Joplin, one package at a time</twitter-text></item><item><title><![CDATA[Joplin Cloud is now part of the Joplin company]]></title><description><![CDATA[<p>As some of you may know Joplin Cloud so far has been operating under my own single-person limited company in the UK. This was mostly for convenience since it meant I could get things going quickly without having to setup a special structure for it.</p>
<p>Now that Joplin Cloud is becoming more mature however a proper company, simply called Joplin, has been created. This company will be based in France, and will be used mainly to handle the commercial part of the project, which currently is mostly Joplin Cloud. I'm still heading the company so there won't be any major change to the way the project is managed.</p>
<h2>What does it mean for Joplin Cloud?<a name="what-does-it-mean-for-joplin-cloud" href="#what-does-it-mean-for-joplin-cloud" class="heading-anchor">🔗</a></h2>
<p>There will be no significant change - the website ownership simply moves from one company in the UK to one in France. The new company is still owned by myself so I will keep following the same roadmap.</p>
@@ -190,29 +211,29 @@
<p>Longer term I would like to create a non-profit organisation to handle the open source applications and to make decisions about the project, as well as to decide how to allocate any funding we receive (for example from GSoC).</p>
<h2>Looking forward<a name="looking-forward" href="#looking-forward" class="heading-anchor">🔗</a></h2>
<p>Those past 6 years of developing Joplin have been an exciting and rewarding experience, thank you to all of you of the friendly and vibrant Joplin community for your contribution toward making Joplin the software it is today, and looking forward to continuing the journey together!</p>
]]></description><link>https://joplinapp.org/news/20221012-Joplin-Company/</link><guid isPermaLink="false">20221012-Joplin-Company</guid><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate><twitter-text>Joplin Cloud is now operated by the Joplin company! More info on the announcement post.</twitter-text></item><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p>
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet/</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
]]></description><link>https://joplinapp.org/news/20221012-Joplin-Company</link><guid isPermaLink="false">20221012-Joplin-Company</guid><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate><twitter-text>Joplin Cloud is now operated by the Joplin company! More info on the announcement post.</twitter-text></item><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p>
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
<p>More information on the official Meetup page:</p>
<p><a href="https://www.meetup.com/joplin/events/287611873/">https://www.meetup.com/joplin/events/287611873/</a></p>
]]></description><link>https://joplinapp.org/news/20220808-first-meetup/</link><guid isPermaLink="false">20220808-first-meetup</guid><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><twitter-text>Joplin will have its first Meetup on 30 August! Come and join us at the Old Thameside Inn next to London Bridge! https://www.meetup.com/joplin/events/287611873/</twitter-text></item><item><title><![CDATA[Joplin 2.8 is available!]]></title><description><![CDATA[<p>As always a lot of changes and new features in this new version available on both desktop and mobile.</p>
<h1>Multiple profile support<a name="multiple-profile-support" href="#multiple-profile-support" class="heading-anchor">🔗</a></h1>
]]></description><link>https://joplinapp.org/news/20220808-first-meetup</link><guid isPermaLink="false">20220808-first-meetup</guid><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><twitter-text>Joplin will have its first Meetup on 30 August! Come and join us at the Old Thameside Inn next to London Bridge! https://www.meetup.com/joplin/events/287611873/</twitter-text></item><item><title><![CDATA[Joplin 2.8 is available!]]></title><description><![CDATA[<p>As always a lot of changes and new features in this new version available on both desktop and mobile.</p>
<h2>Multiple profile support<a name="multiple-profile-support" href="#multiple-profile-support" class="heading-anchor">🔗</a></h2>
<p>Perhaps the most visible change in this version is the support for multiple profiles. You can now create as many application profile as you wish, each with their own settings, and easily switch from one to another. The main use case is to support for example a &quot;work&quot; profile and a &quot;personal&quot; profile, to allow you to keep things independent, and each profile can sync with a different sync target.</p>
<p>To create a new profile, open <strong>File &gt; Switch profile</strong> and select <strong>Create new profile</strong>, enter the profile name and press OK. The app will automatically switch to this new profile, which you can now configure.</p>
<p>To switch back to the previous profile, again open <strong>File &gt; Switch profile</strong> and select <strong>Default</strong>.</p>
<p>Note that profiles all share certain settings, such as language, font size, theme, etc. This is done so that you don't have reconfigure every details when switching profiles. Other settings such as sync configuration is per profile.</p>
<p>The feature is available on desktop only for now, and should be ported to mobile relatively soon.</p>
<h1>Save Mermaid graph as PNG/SVG<a name="save-mermaid-graph-as-png-svg" href="#save-mermaid-graph-as-png-svg" class="heading-anchor">🔗</a></h1>
<h2>Save Mermaid graph as PNG/SVG<a name="save-mermaid-graph-as-png-svg" href="#save-mermaid-graph-as-png-svg" class="heading-anchor">🔗</a></h2>
<p>This convenient feature allows exporting a Mermaid graph as a PNG or SVG image, or allows copying the image as a DataUrl, which can then be pasted in any compatible text editor. Thanks Asrient for implementing this!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220606-mermaid-as-png.png" alt=""></p>
<h1>Publish a mini-website using Joplin Cloud<a name="publish-a-mini-website-using-joplin-cloud" href="#publish-a-mini-website-using-joplin-cloud" class="heading-anchor">🔗</a></h1>
<h2>Publish a mini-website using Joplin Cloud<a name="publish-a-mini-website-using-joplin-cloud" href="#publish-a-mini-website-using-joplin-cloud" class="heading-anchor">🔗</a></h2>
<p>Joplin Cloud now supports publishing a note &quot;recursively&quot;, which means the notes and all the notes it is linked to. This allows easily publishing a simple website made of multiples and images.</p>
<p>To make use of this feature, simply select <strong>Also publish linked notes</strong> when publishing a note.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220606-publish-website.png" alt=""></p>
<h1>And more!<a name="and-more" href="#and-more" class="heading-anchor">🔗</a></h1>
<p>In total there are 38 changes to improve the app reliability, security and usability. Full changelog is at <a href="https://joplinapp.org/changelog/">https://joplinapp.org/changelog/</a></p>
]]></description><link>https://joplinapp.org/news/20220606-release-2-8/</link><guid isPermaLink="false">20220606-release-2-8</guid><pubDate>Mon, 06 Jun 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin received 6 Contributor Projects for GSoC 2022!]]></title><description><![CDATA[<p>We are glad to announce that Google allocated us six projects this year for Google Summer of Code! So this is six contributors who will be working on various parts of the apps, both desktop and mobile, over the summer.</p>
<h2>And more!<a name="and-more" href="#and-more" class="heading-anchor">🔗</a></h2>
<p>In total there are 38 changes to improve the app reliability, security and usability. Full changelog is at <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></p>
]]></description><link>https://joplinapp.org/news/20220606-release-2-8</link><guid isPermaLink="false">20220606-release-2-8</guid><pubDate>Mon, 06 Jun 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin received 6 Contributor Projects for GSoC 2022!]]></title><description><![CDATA[<p>We are glad to announce that Google allocated us six projects this year for Google Summer of Code! So this is six contributors who will be working on various parts of the apps, both desktop and mobile, over the summer.</p>
<p>Over the next few weeks, till 13 June, will be the Community Bonding Period during which GSoC contributors get to know mentors, read documentation, and get up to speed to begin working on their projects.</p>
<p>Here's the full list of projects, contributors and mentors.</p>
<table class="table">
@@ -256,24 +277,24 @@
</tr>
</tbody>
</table>
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors/</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer! #GSoC2022</twitter-text></item><item><title><![CDATA[GSoC "Contributor Proposals" phase is starting now!]]></title><description><![CDATA[<p>The &quot;Contributor Proposals&quot; phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to <a href="https://summerofcode.withgoogle.com/">https://summerofcode.withgoogle.com/</a></p>
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item><item><title><![CDATA[GSoC "Contributor Proposals" phase is starting now!]]></title><description><![CDATA[<p>The &quot;Contributor Proposals&quot; phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to <a href="https://summerofcode.withgoogle.com/">https://summerofcode.withgoogle.com/</a></p>
<p>When it's done, please also let us know by posting an update on your forum introduction post.</p>
<p>If you haven't created a pull request yet, it's still time to create one. Doing so will greatly increase your chances of being selected!</p>
]]></description><link>https://joplinapp.org/news/20220405-gsoc-contributor-proposals/</link><guid isPermaLink="false">20220405-gsoc-contributor-proposals</guid><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin participates in Google Summer of Code 2022!]]></title><description><![CDATA[<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220308-gsoc-banner.png" alt=""></p>
]]></description><link>https://joplinapp.org/news/20220405-gsoc-contributor-proposals</link><guid isPermaLink="false">20220405-gsoc-contributor-proposals</guid><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin participates in Google Summer of Code 2022!]]></title><description><![CDATA[<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220308-gsoc-banner.png" alt=""></p>
<p>For the third year, Joplin has been selected as a <strong>Google Summer of Code</strong> mentor organisation! We look forward to start working with the contributors on some great new projects. This year's main themes are:</p>
<ul>
<li><strong>Mobile and tablet development</strong> - we want to improve the mobile/tablet application on iOS and Android.</li>
<li><strong>Plugin and external apps</strong> - leverage the Joplin API to create plugins and external apps.</li>
<li>And of course contributors are welcome to suggest their own ideas.</li>
</ul>
<p>Our full idea list is available here: <a href="https://joplinapp.org/gsoc2022/ideas/">GSoC 2022 idea list</a></p>
<p>Our full idea list is available here: <a href="https://joplinapp.org/help/dev/gsoc/gsoc2022/ideas">GSoC 2022 idea list</a></p>
<p>In the coming month (<strong>March 7 - April 3</strong>), contributors will start getting involved in the forum and start discussing project ideas with the mentors and community. It's also a good time to start looking at Joplin's source code, perhaps work on fixing bugs or implement small features to get familiar with the source code, and to show us your skills.</p>
<p>One difference with previous years is that anyone, not just students, are allowed to participate.</p>
<p>Additionally, last year Google only allowed smaller projects, while this year they allow again small and large projects, so we've indicated this in the idea list - the small ones are <strong>175 hours</strong>, and the large ones <strong>350 hours</strong>.</p>
]]></description><link>https://joplinapp.org/news/20220308-gsoc2022-start/</link><guid isPermaLink="false">20220308-gsoc2022-start</guid><pubDate>Tue, 08 Mar 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin 2.7 is available!]]></title><description><![CDATA[<p>This new release is largely focused on bug fixing and optimising various parts of the apps. There's about 26 improvements and 25 bugs and security fixes included - as always many of these apply to both the mobile and desktop app (see the <a href="https://joplinapp.org/changelog/">desktop changelog</a> and <a href="https://joplinapp.org/changelog_android/">mobile changelog</a>).</p>
]]></description><link>https://joplinapp.org/news/20220308-gsoc2022-start</link><guid isPermaLink="false">20220308-gsoc2022-start</guid><pubDate>Tue, 08 Mar 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin 2.7 is available!]]></title><description><![CDATA[<p>This new release is largely focused on bug fixing and optimising various parts of the apps. There's about 26 improvements and 25 bugs and security fixes included - as always many of these apply to both the mobile and desktop app (see the <a href="https://joplinapp.org/help/about/changelog/desktop">desktop changelog</a> and <a href="https://joplinapp.org/help/about/changelog/android/">mobile changelog</a>).</p>
<p>Many thanks to all the contributors who helped create this release!</p>
<p>Below are some of the more noticeable changes:</p>
<h1>Notebook custom icons<a name="notebook-custom-icons" href="#notebook-custom-icons" class="heading-anchor">🔗</a></h1>
<h2>Notebook custom icons<a name="notebook-custom-icons" href="#notebook-custom-icons" class="heading-anchor">🔗</a></h2>
<p>Since version 2.6 it was possible to assign an emoji icon to a notebook, and with this new version it's now possible to assign any custom icon. The icon may be a PNG or JPG file of any size. The app will then import the file and resize it to the correct size. To use a custom icon, follow these steps:</p>
<p>Right-click on a notebook, and select &quot;Edit&quot;:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220224-edit-notebook.png" alt=""></p>
@@ -282,13 +303,13 @@
<p>Click &quot;OK&quot; and the icon will now appear next to the notebook:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220224-notebook-icon.png" alt=""></p>
<p>The icon can be changed only from the desktop application at the moment, but it will sync and be displayed correctly on the mobile app too.</p>
<h1>Plugin API improvements<a name="plugin-api-improvements" href="#plugin-api-improvements" class="heading-anchor">🔗</a></h1>
<h2>Plugin API improvements<a name="plugin-api-improvements" href="#plugin-api-improvements" class="heading-anchor">🔗</a></h2>
<p>This version also includes a number of improvements to the plugin API, in particular it is now easier to customise the editor context menu from a plugin and dynamically add items to it depending on the context. For example, with the Rich Markdown plugin it will be possible to right-click on an image and open it, or copy it to the clipboard.</p>
<p>A few additional functions have also been added to make plugin development simpler - in particular a command to open any item, whether it's a notebook, note, tag or attachement; and functions to work with attachements, in particular to reveal an attachement in the system file explorer, and to track changes to an attachement.</p>
]]></description><link>https://joplinapp.org/news/20220224-release-2-7/</link><guid isPermaLink="false">20220224-release-2-7</guid><pubDate>Thu, 24 Feb 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Automatic deletion of disabled accounts on Joplin Cloud]]></title><description><![CDATA[<p>As of 15 Feb 2022, disabled accounts on Joplin Cloud will be automatically deleted after 90 days. A disabled account is one where the Stripe subscription has been cancelled either by the user or automatically (eg for unpaid invoices).</p>
]]></description><link>https://joplinapp.org/news/20220224-release-2-7</link><guid isPermaLink="false">20220224-release-2-7</guid><pubDate>Thu, 24 Feb 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Automatic deletion of disabled accounts on Joplin Cloud]]></title><description><![CDATA[<p>As of 15 Feb 2022, disabled accounts on Joplin Cloud will be automatically deleted after 90 days. A disabled account is one where the Stripe subscription has been cancelled either by the user or automatically (eg for unpaid invoices).</p>
<p>Although it is an automated system, I will manually verify each account that's queued for deletion over the next few days for additional safety (for now everything's working as expected).</p>
<p>When an account is queued for deletion, all notes, notebooks, tags, etc are removed from the system within 2 days, and permanently deleted within 7 days. User information, in particular email and full name will be removed from the system within 2 days, but archived for an additional 90 days for legal reasons, after which they will be deleted too.</p>
]]></description><link>https://joplinapp.org/news/20220215-142000/</link><guid isPermaLink="false">20220215-142000</guid><pubDate>Tue, 15 Feb 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin 2.6 is available!]]></title><description><![CDATA[<p>Many changes in this new release, available on mobile, desktop and CLI:</p>
]]></description><link>https://joplinapp.org/news/20220215-142000</link><guid isPermaLink="false">20220215-142000</guid><pubDate>Tue, 15 Feb 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin 2.6 is available!]]></title><description><![CDATA[<p>Many changes in this new release, available on mobile, desktop and CLI:</p>
<p><strong>Per-notebook sort order and sort buttons</strong></p>
<p>This new feature adds a number of changes to the way notes are sorted. The most visible one is the addition of a sort button above the note list - it allows sorting by modification date, creation date, title or by custom order, in either ascending or descending order:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20211217-120324_0.png" alt=""></p>
@@ -315,21 +336,8 @@
<p>Exporting a single note as HTML is now more user friendly as all images, scripts, styles and other attachments are all packed into a single HTML file (Previously it would create multiples files and directories). This makes it easier to share the complete note with someone who doesn't have Joplin.</p>
<p><strong>Other changes and bug fixes</strong></p>
<p>This release includes a total of 19 new features and improvements and 16 bug fixes. See the 2.6.x changelogs for more details:</p>
<p><a href="https://joplinapp.org/changelog/">https://joplinapp.org/changelog/</a></p>
<p><a href="https://joplinapp.org/changelog_android/">https://joplinapp.org/changelog_android/</a></p>
<p><a href="https://joplinapp.org/changelog_ios/">https://joplinapp.org/changelog_ios/</a></p>
<p><a href="https://joplinapp.org/changelog_cli/">https://joplinapp.org/changelog_cli/</a></p>
]]></description><link>https://joplinapp.org/news/20211217-120324/</link><guid isPermaLink="false">20211217-120324</guid><pubDate>Fri, 17 Dec 2021 12:03:24 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Potential breaking change in next Joplin Server update (2.5.10)]]></title><description><![CDATA[<p>Just a head up that the next Joplin Server update could potentially include a breaking change, depending on your data.</p>
<p>One of the database migration is going to add an &quot;owner_id&quot; column to the &quot;items&quot; table (where all notes, notebooks, etc. are stored), and automatically populate it. Normally that shouldn't take too long but you might want to make sure you won't need the server right away when you process this.</p>
<p>The second database migration will add a unique constraint on items.name and items.owner_id and that's where the breaking change might be. Normally this data is already unique because that's enforced by the application but in some rare cases, due a race condition, there could be duplicate data in there. If that happens the migration will fail and the server will not start.</p>
<p>If that happens, you'll need to decide what to do with the data, as it's not possible to automatically decide. You can find all duplicates using this query:</p>
<p><code><strong>select</strong> count(<em>), name, owner_id<br>
<strong>from</strong> items <strong>group</strong> <strong>by</strong> name, owner_id<br>
<strong>having</strong> count(</em>) &gt; 1;</code></p>
<p>Once you have the list of IDs you have a few options:</p>
<ul>
<li>Find the corresponding item in Joplin (it can unfortunately be anything - a note, resource, folder, etc.), then delete it and sync.</li>
<li>Or, just delete the data directly in the database. You'll want to delete the corresponding item_id from the user_items table too.</li>
</ul>
<p>But really in most cases you should be fine. Especially if you don't have that many notes it's unlikely you have duplicates.</p>
]]></description><link>https://joplinapp.org/news/20211102-150403/</link><guid isPermaLink="false">20211102-150403</guid><pubDate>Tue, 02 Nov 2021 15:04:03 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
<p><a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></p>
<p><a href="https://joplinapp.org/help/about/changelog/android/">https://joplinapp.org/help/about/changelog/android/</a></p>
<p><a href="https://joplinapp.org/help/about/changelog/ios/">https://joplinapp.org/help/about/changelog/ios/</a></p>
<p><a href="https://joplinapp.org/help/about/changelog/cli/">https://joplinapp.org/help/about/changelog/cli/</a></p>
]]></description><link>https://joplinapp.org/news/20211217-120324</link><guid isPermaLink="false">20211217-120324</guid><pubDate>Fri, 17 Dec 2021 12:03:24 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -172,7 +172,7 @@
</p>
<br />
<p>
<a translate href="{{baseUrl}}/clipper/" class="button-link btn-blue">Get the clipper</a>
<a translate href="{{baseUrl}}/help/apps/clipper/" class="button-link btn-blue">Get the clipper</a>
</p>
</div>
</div>
@@ -259,7 +259,7 @@
</p>
<br />
<p>
<a translate href="{{baseUrl}}/e2ee/" class="button-link btn-blue">More about E2EE</a>
<a translate href="{{baseUrl}}/help/apps/sync/e2ee" class="button-link btn-blue">More about E2EE</a>
</p>
</div>
</div>

View File

@@ -86,11 +86,7 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
{{> footer}}
</div>
<script
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
rel="preload"
as="script"
></script>
<script src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js" rel="preload" as="script"></script>
<script src="{{{assetUrls.js.script}}}"></script>
{{> analytics}}

View File

@@ -16,7 +16,7 @@
<a href="{{baseUrl}}/news/" class="fw500">News</a>
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
<!-- <a href="{{baseUrl}}/cn/" class="fw500">中文</a> -->
<!--
<div class="dropdown language-switcher">
@@ -39,7 +39,7 @@
</div>
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
{{> twitterLink}}
<a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a>
<!-- <a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a> -->
{{> joplinCloudButton}}
{{> supportButton}}

View File

@@ -138,6 +138,14 @@
} else {
applyPeriod('yearly');
}
$('.feature-description').hide();
$('.feature-title').click((event) => {
event.preventDefault();
const featureId = event.currentTarget.getAttribute('data-id');
$('.feature-description-' + featureId).toggle(200);
});
});
</script>
</div>

View File

@@ -71,7 +71,7 @@ EXPOSE ${APP_PORT}
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals
WORKDIR /home/$user/packages/server
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/app.js"]
CMD ["yarn", "start-prod"]
# Build-time metadata
# https://github.com/opencontainers/image-spec/blob/master/annotations.md

546
README.md
View File

@@ -2,66 +2,31 @@
[![Donate using PayPal](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=E8JMYD2LQ8MMA&no_recurring=0&item_name=I+rely+on+donations+to+maintain+and+improve+the+Joplin+open+source+project.+Thank+you+for+your+help+-+it+makes+a+difference%21&currency_code=EUR) [![Sponsor on GitHub](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/GitHub-Badge.svg)](https://github.com/sponsors/laurent22/) [![Become a patron](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Patreon-Badge.svg)](https://www.patreon.com/joplin) [![Donate using IBAN](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-IBAN.svg)](https://joplinapp.org/donate/#donations)
<!-- DONATELINKS -->
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin** is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" style="margin-right:15px"/>
Notes exported from Evernote [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
**Joplin** is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](https://github.com/laurent22/joplin/blob/dev/readme/apps/markdown.md).
The notes can be securely [synchronised](#synchronisation) using [end-to-end encryption](#encryption) with various cloud services including Nextcloud, Dropbox, OneDrive and [Joplin Cloud](https://joplinapp.org/plans/).
Notes exported from Evernote [can be imported](https://github.com/laurent22/joplin/blob/dev/readme/apps/import_export.md) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
Joplin is "offline first", which means you always have all your data on your phone or computer. This ensures that your notes are always accessible, whether you have an internet connection or not.
The notes can be securely [synchronised](https://github.com/laurent22/joplin/blob/dev/readme/apps/sync/index.md) using [end-to-end encryption](https://github.com/laurent22/joplin/blob/dev/readme/apps/sync/e2ee.md) with various cloud services including Nextcloud, Dropbox, OneDrive and [Joplin Cloud](https://joplinapp.org/plans/).
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
The application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
The application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/apps/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/home-top-img.png" style="max-width: 100%; max-height: 35em;"></div>
# Installation
# Help and documentation
Three types of applications are available: for **desktop** (Windows, macOS and Linux), for **mobile** (Android and iOS) and for **terminal** (Windows, macOS, Linux and FreeBSD). All the applications have similar user interfaces and can synchronise with each other.
For more information about the applications, see the [full Joplin documentation](https://joplinapp.org)
## Desktop applications
# Donations
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-Setup-2.12.17.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-2.12.17.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-2.12.17.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.12.17/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
<pre><code style="word-break: break-all">wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash</code></pre>
The install and update script supports the [following flags](https://github.com/laurent22/joplin/blob/dev/Joplin_install_and_update.sh#L50) (around line 50 at the time of this writing).
## Mobile applications
Operating System | Download | Alt. Download
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v2.12.3/joplin-v2.12.3.apk?source=JoplinWebsite&type=New)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
## Terminal application
Operating system | Method
-----------------|----------------
macOS, Linux, or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 12+](https://nodejs.org/en/download/package-manager/).<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
To start it, type `joplin`.
For usage information, please refer to the full [Joplin Terminal Application Documentation](https://joplinapp.org/terminal/).
## Web Clipper
The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. For more information on how to install and use it, see the [Web Clipper Help Page](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md).
## Unofficial Alternative Distributions
There are a number of unofficial alternative Joplin distributions. If you do not want to or cannot use appimages or any of the other officially supported releases then you may wish to consider these.
However these come with a caveat in that they are not officially supported so certain issues may not be supportable by the main project. Rather support requests, bug reports and general advice would need to go to the maintainers of these distributions.
A community maintained list of these distributions can be found here: [Unofficial Joplin distributions](https://discourse.joplinapp.org/t/unofficial-alternative-joplin-distributions/23703)
Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/readme/about/donate.md) for information on how to support the development of Joplin.
# Sponsors
@@ -80,428 +45,7 @@ A community maintained list of these distributions can be found here: [Unofficia
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <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/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | |
<!-- SPONSORS-GITHUB -->
<!-- TOC -->
# Table of contents
- Applications
- [Desktop application](https://github.com/laurent22/joplin/blob/dev/readme/desktop.md)
- [Mobile applications](https://github.com/laurent22/joplin/blob/dev/readme/mobile.md)
- [Terminal application](https://github.com/laurent22/joplin/blob/dev/readme/terminal.md)
- [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md)
- Support
- [Joplin Forum](https://discourse.joplinapp.org)
- [Markdown Guide](https://github.com/laurent22/joplin/blob/dev/readme/markdown.md)
- [How to enable end-to-end encryption](https://github.com/laurent22/joplin/blob/dev/readme/e2ee.md)
- [What is a conflict?](https://github.com/laurent22/joplin/blob/dev/readme/conflict.md)
- [How to enable debug mode](https://github.com/laurent22/joplin/blob/dev/readme/debugging.md)
- [About the Rich Text editor limitations](https://github.com/laurent22/joplin/blob/dev/readme/rich_text_editor.md)
- [External links](https://github.com/laurent22/joplin/blob/dev/readme/external_links.md)
- [FAQ](https://github.com/laurent22/joplin/blob/dev/readme/faq.md)
- Joplin Cloud
- [Sharing a notebook](https://github.com/laurent22/joplin/blob/dev/readme/share_notebook.md)
- [Publishing a note](https://github.com/laurent22/joplin/blob/dev/readme/publish_note.md)
- [Email to Note](https://github.com/laurent22/joplin/blob/dev/readme/email_to_note.md)
- Joplin API - Get Started
- [Joplin API Overview](https://github.com/laurent22/joplin/blob/dev/readme/api/overview.md)
- [Plugin development](https://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md)
- [Plugin tutorial](https://github.com/laurent22/joplin/blob/dev/readme/api/tutorials/toc_plugin.md)
- Joplin API - References
- [Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
- [Data API](https://github.com/laurent22/joplin/blob/dev/readme/api/references/rest_api.md)
- [Plugin manifest](https://github.com/laurent22/joplin/blob/dev/readme/api/references/plugin_manifest.md)
- [Plugin loading rules](https://github.com/laurent22/joplin/blob/dev/readme/api/references/plugin_loading_rules.md)
- [Plugin theming](https://github.com/laurent22/joplin/blob/dev/readme/api/references/plugin_theming.md)
- Development
- [How to build the apps](https://github.com/laurent22/joplin/blob/dev/BUILD.md)
- [Writing a technical spec](https://github.com/laurent22/joplin/blob/dev/readme/technical_spec.md)
- [Desktop application styling](https://github.com/laurent22/joplin/blob/dev/readme/spec/desktop_styling.md)
- [Note history spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/history.md)
- [Synchronisation spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync.md)
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
- [Synchronous Scroll spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_scroll.md)
- [Overall Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/architecture.md)
- [Plugin Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/plugins.md)
- [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md)
- [E2EE: Technical spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee.md)
- [E2EE: Workflow](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee/workflow.md)
- [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md)
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
- [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md)
- [Read-only items](https://github.com/laurent22/joplin/blob/dev/readme/spec/read_only.md)
- Google Summer of Code 2022
- [Google Summer of Code 2022](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/index.md)
- [How to submit a GSoC pull request](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/pull_request_guidelines.md)
- [Project Ideas](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/ideas.md)
- About
- [Changelog (Desktop App)](https://github.com/laurent22/joplin/blob/dev/readme/changelog.md)
- [Changelog (Android)](https://github.com/laurent22/joplin/blob/dev/readme/changelog_android.md)
- [Changelog (iOS)](https://github.com/laurent22/joplin/blob/dev/readme/changelog_ios.md)
- [Changelog (CLI App)](https://github.com/laurent22/joplin/blob/dev/readme/changelog_cli.md)
- [Changelog (Server)](https://github.com/laurent22/joplin/blob/dev/readme/changelog_server.md)
- [Guiding principles](https://github.com/laurent22/joplin/blob/dev/readme/principles.md)
- [Stats](https://github.com/laurent22/joplin/blob/dev/readme/stats.md)
- [Brand guidelines](https://joplinapp.org/brand)
- [Release cycle](https://github.com/laurent22/joplin/blob/dev/readme/release_cycle.md)
- [Donate](https://github.com/laurent22/joplin/blob/dev/readme/donate.md)
<!-- TOC -->
# Features
- Desktop, mobile and terminal applications.
- [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md) for Firefox and Chrome.
- End To End Encryption (E2EE).
- Note history (revisions).
- Synchronisation with various services, including Nextcloud, Dropbox, WebDAV and OneDrive.
- Offline first, so the entire data is always available on the device even without an internet connection.
- Import Enex files (Evernote export format) and Markdown files.
- Export JEX files (Joplin Export format) and raw files.
- Support notes, to-dos, tags and notebooks.
- Sort notes by multiple criteria - title, updated time, etc.
- Support for alarms (notifications) in mobile and desktop applications.
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications. Support for extra features such as math notation and checkboxes.
- Choice of both Markdown and Rich Text (WYSIWYG) editors.
- File attachment support - images are displayed, other files are linked and can be opened in the relevant application.
- Inline display of PDF, video and audio files.
- Goto Anything feature.
- Search functionality.
- Geo-location support.
- Supports multiple languages.
- External editor support - open notes in your favorite external editor with one click in Joplin.
- Extensible functionality through plugin and data APIs.
- Custom CSS support for customisation of both the rendered markdown and overall user interface.
- Customisable layout allows toggling, movement and sizing of various elements.
- Keyboard shortcuts are editable and allow binding of most Joplin commands with export/import functionality.
- Multiple profile support.
# Importing
## Importing from Evernote
Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, resources (attached files) and note metadata (such as author, geo-location, etc.) via ENEX files. In terms of data, the only two things that might slightly differ are:
- Recognition data - Evernote images, in particular scanned (or photographed) documents have [recognition data](https://en.wikipedia.org/wiki/Optical_character_recognition) associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, should it become supported in the search tool or other parts of Joplin, it should be possible to regenerate this recognition data since the actual image would still be available.
- Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be lost - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting. If it is essential that this extra data is preserved then Joplin also allows import of ENEX files as HTML.
To import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then follow these steps:
In the **desktop application**, open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc.
In the **terminal application**, in [command-line mode](https://github.com/laurent22/joplin/blob/dev/readme/terminal.md#command-line-mode), type `import /path/to/file.enex`. This will import the notes into a new notebook named after the filename.
## Importing from Markdown files
Joplin can import notes from plain Markdown file. You can either import a complete directory of Markdown files or individual files.
In the **desktop application**:
* **File import**: Go to File > Import > MD - Markdown (file) and select the Markdown file. This file will then be imported to the currently selected Notebook.
* **Directory import**: Go to File > Import > MD - Markdown (directory) and select the top level of the directory that is being imported. Directory (folder) structure will be preserved in the Notebook > Subnotebook > Note structure within Joplin.
In the **terminal application**, in [command-line mode](https://github.com/laurent22/joplin/blob/dev/readme/terminal.md#command-line-mode), type `import --format md /path/to/file.md` or `import --format md /path/to/directory/`.
## Importing from other applications
In general the way to import notes from any application into Joplin is to convert the notes to ENEX files (Evernote format) and to import these ENEX files into Joplin using the method above. Most note-taking applications support ENEX files so it should be relatively straightforward. For help about specific applications, see below:
* Standard Notes: Please see [this tutorial](https://programadorwebvalencia.com/migrate-notes-from-standard-notes-to-joplin/)
* Tomboy Notes: Export the notes to ENEX files [as described here](https://askubuntu.com/questions/243691/how-can-i-export-my-tomboy-notes-into-evernote/608551) for example, and import these ENEX files into Joplin.
* OneNote: First [import the notes from OneNote into Evernote](https://discussion.evernote.com/topic/107736-is-there-a-way-to-import-from-onenote-into-evernote-on-the-mac/). Then export the ENEX file from Evernote and import it into Joplin.
* NixNote: Synchronise with Evernote, then export the ENEX files and import them into Joplin. More info [in this thread](https://discourse.joplinapp.org/t/import-from-nixnote/183/3).
# Exporting
Joplin can export to the JEX format (Joplin Export file), which is a tar file that can contain multiple notes, notebooks, etc. This is a lossless format in that all the notes, but also metadata such as geo-location, updated time, tags, etc. are preserved. This format is convenient for backup purposes and can be re-imported into Joplin. A "raw" format is also available. This is the same as the JEX format except that the data is saved to a directory and each item represented by a single file.
Joplin is also capable of exporting to a number of other formats including HTML and PDF which can be done for single notes, notebooks or everything.
# Synchronisation
One of the goals of Joplin is to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another.
Currently, synchronisation is possible with Joplin Cloud, Nextcloud, S3, WebDAV, Dropbox, OneDrive or the local filesystem. To enable synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually. Joplin will background sync automatically after any content change is made on the local application.
If the **terminal client** has been installed, it is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at a regular interval. For example, this would do it every 30 minutes:
` */30 * * * * /path/to/joplin sync`
## Nextcloud synchronisation
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/nextcloud-logo-background.png" width="100" align="left"> <a href="https://nextcloud.com/">Nextcloud</a> is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin. The platform is also well supported and with a strong community, so it is likely to be around for a while - since it's open source anyway, it is not a service that can be closed, it can exist on a server for as long as one chooses.
In the **desktop application** or **mobile application**, go to the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md) and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, click on Settings in the bottom left corner of the page, in Nextcloud), this is normally `https://example.com/nextcloud/remote.php/webdav/Joplin` (**make sure to create the "Joplin" directory in Nextcloud**), and set the username and password. If it does not work, please [see this explanation](https://github.com/laurent22/joplin/issues/61#issuecomment-373282608) for more details.
In the **terminal application**, you will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
:config sync.5.path https://example.com/nextcloud/remote.php/webdav/Joplin
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
:config sync.target 5
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above (for the **terminal application** you will need to select sync target 6 rather than 5)
WebDAV-compatible services that are known to work with Joplin:
- [Apache WebDAV Module](https://httpd.apache.org/docs/current/mod/mod_dav.html)
- [DriveHQ](https://www.drivehq.com)
- [Fastmail](https://www.fastmail.com/)
- [HiDrive](https://www.strato.fr/stockage-en-ligne/) from Strato. [Setup help](https://github.com/laurent22/joplin/issues/309)
- [Nginx WebDAV Module](https://nginx.org/en/docs/http/ngx_http_dav_module.html)
- [Nextcloud](https://nextcloud.com/)
- [OwnCloud](https://owncloud.org/)
- [Seafile](https://www.seafile.com/)
- [Stack](https://www.transip.nl/stack/)
- [Synology WebDAV Server](https://www.synology.com/en-us/dsm/packages/WebDAVServer)
- [WebDAV Nav](https://www.schimera.com/products/webdav-nav-server/), a macOS server.
- [Zimbra](https://www.zimbra.com/)
## Dropbox synchronisation
When syncing with Dropbox, Joplin creates a sub-directory in Dropbox, in `/Apps/Joplin` and reads/writes the notes and notebooks in it. The application does not have access to anything outside this directory.
In the **desktop application** or **mobile application**, select "Dropbox" as the synchronisation target in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md) (it is selected by default). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar and follow the instructions.
In the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and reads/writes the notes and notebooks in it. The application does not have access to anything outside this directory.
In the **desktop application** or **mobile application**, select "OneDrive" as the synchronisation target in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar and follow the instructions.
In the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).
## S3 synchronisation
As of Joplin 2.x.x, Joplin supports multiple S3 providers. We expose some options that will need to be configured depending on your provider of choice. We have tested with UpCloud, AWS, and Linode. others should work as well.
In the **desktop application** or **mobile application**, select "S3 (Beta)" as the synchronisation target in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md).
- **S3 Bucket:** The name of your Bucket, such as `joplin-bucket`
- **S3 URL:** Fully qualified URL; For AWS this should be `https://s3.<regionName>.amazonaws.com/`
- **S3 Access Key & S3 Secret Key:** The User's programmatic access key. To create a new key & secret on AWS, visit [IAM Security Credentials](https://console.aws.amazon.com/iam/home#/security_credentials). For other providers follow their documentation.
- **S3 Region:** Some providers require you to provide the region of your bucket. This is usually in the form of "eu-west1" or something similar depending on your region. For providers that do not require a region, you can leave it blank.
- **Force Path Style**: This setting enables Joplin to talk to S3 providers using an older style S3 Path. Depending on your provider you may need to try with this on and off.
While creating a new Bucket for Joplin, disable **Bucket Versioning**, enable **Block all public access** and enable **Default encryption** with `Amazon S3 key (SSE-S3)`. Some providers do not expose these options, and it could create a syncing problem. Do attempt and report back so we can update the documentation appropriately.
To add a **Bucket Policy** from the AWS S3 Web Console, navigate to the **Permissions** tab. Temporarily disable **Block all public access** to edit the Bucket policy, something along the lines of:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::joplin-bucket",
"arn:aws:s3:::joplin-bucket/*"
]
}
]
}
```
### Configuration settings for tested providers
All providers will require a bucket, Access Key, and Secret Key.
If you provide a configuration and you receive "success!" on the "check config" then your S3 sync should work for your provider. If you do not receive success, you may need to adjust your settings, or save them, restart the app, and attempt a sync. This may reveal more clear error messaging that will help you deduce the problem.
### AWS
- URL: `https://s3.<region>.amazonaws.com/` (fill in your region, a complete list of endpoint adresses can be found [here](https://docs.aws.amazon.com/general/latest/gr/s3.html))
- Region: required
- Force Path Style: unchecked
### Linode
- URL: `https://<region>.linodeobjects.com` (region is in the URL provided by Linode; this URL is also the same as the URL provided by Linode with the bucket name removed)
- Region: Anything you want to type, can't be left empty
- Force Path Style: unchecked
### UpCloud
- URL: `https://<account>.<region>.upcloudobjects.com` (They will provide you with multiple URLs, the one that follows this pattern should work.)
- Region: required
- Force Path Style: unchecked
# Encryption
Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](https://github.com/laurent22/joplin/blob/dev/readme/e2ee.md) for more information about this feature and how to enable it.
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee.md).
# Note history
The Joplin applications automatically save previous versions of your notes at regular intervals. These versions are synced across devices and can be viewed from the desktop application. To do so, click on the "Information" button on a note, then click on "Previous version of this note". From this screen you can view the previous versions of the note as well as restore any of them.
This feature can be disabled from the "Note history" section in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md), and it is also possible to change for how long the history of a note is saved.
More information please see the [Note History page](https://github.com/laurent22/joplin/blob/dev/readme/note_history.md).
# External text editor
Joplin notes can be opened and edited using an external editor of your choice. It can be a simple text editor like Notepad++ or Sublime Text or an actual Markdown editor like Typora. In that case, images will also be displayed within the editor. To open the note in an external editor, click on the icon in the toolbar or press Ctrl+E (or Cmd+E). Your default text editor will be used to open the note. If needed, you can also specify the editor directly in the General Options, under "Text editor command".
# Attachments
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the attachment, clicking on this link will open the file in the default application. In the case of audio, video and pdf files, these will be displayed inline with the note and so can be viewed or played within Joplin.
In the **desktop application**, files can be attached either by clicking the "Attach file" icon in the editor or via drag and drop. If you prefer to create a link to a local file instead, hold the ALT key while performing the drag and drop operation. You can also copy and paste images directly in the editor via Ctrl+V.
Resources that are not attached to any note will be automatically deleted in accordance to the [Note History](#note-history) settings.
**Important:** Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.
## Downloading attachments
The way the attachments are downloaded during synchronisation can be customised in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md), under "Attachment download behaviour". The default option ("Always") is to download all the attachments, all the time, so that the data is available even when the device is offline. There is also the option to download the attachments manually (option "Manual"), by clicking on it, or automatically (Option "Auto"), in which case the attachments are downloaded only when a note is opened. These options should help saving disk space and network bandwidth, especially on mobile.
# Notifications
In the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:
- **Windows**: >= 8. Make sure the Action Center is enabled on Windows. Task bar balloon for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.
- **macOS**: >= 10.8 or Growl if earlier.
- **Linux**: `notify-send` tool, delivered through packages `notify-osd`, `libnotify-bin` or `libnotify-tools`. GNOME should have this by default, but install `libnotify-tools` if using KDE Plasma.
See [documentation and flow chart for reporter choice](https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md)
On mobile, the alarms will be displayed using the built-in notification system.
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
# Sub-notebooks
Sub-notebooks allow organising multiple notebooks into a tree of notebooks. For example it can be used to regroup all the notebooks related to work, to family or to a particular project under a parent notebook.
![](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/SubNotebooks.png)
- In the **desktop application**, to create a subnotebook, drag and drop it onto another notebook. To move it back to the root, drag and drop it on the "Notebooks" header. Currently only the desktop app can be used to organise the notebooks.
- The **mobile application** supports displaying and collapsing/expanding the tree of notebooks, however it does not currently support moving the subnotebooks to different notebooks.
- The **terminal app** supports displaying the tree of subnotebooks but it does not support collapsing/expanding them or moving the subnotebooks around.
# Markdown
Joplin uses and renders a Github-flavoured Markdown with a few variations and additions. In particular it adds math formula support, interactive checkboxes and support for note links. Joplin also supports Markdown plugins which allow enabling and disabling various advanced Markdown features. Have a look at the [Markdown Guide](https://github.com/laurent22/joplin/blob/dev/readme/markdown.md) for more information.
# Custom CSS
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the `General` page of the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md) for the exact path). This file supports standard CSS syntax. Joplin ***must*** be restarted for the new css to be applied, please ensure that Joplin is not closing to the tray, but is actually exiting. Note that this file is used for both displaying the notes and printing the notes. Be aware how the CSS may look printed (for example, printing white text over a black background is usually not wanted).
The whole UI can be customized by placing a custom editor style file in the profile directory `~/.config/joplin-desktop/userchrome.css`.
Important: userstyle.css and userchrome.css are provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.
# Plugins
The **desktop app** has the ability to extend beyond its standard functionality by the way of plugins. These plugins adhere to the Joplin [plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) and can be installed & configured within the application via the `Plugins` page of the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md).
From this menu you can search for plugins uploaded to the [Joplin plugins](https://github.com/joplin/plugins) repository as well as manual installation of plugins using a 'Joplin Plugin Archive' (*.jpl) file.
Once the application is reloaded the plugins will appear within the plugins menu where they can be toggled on/off or removed entirely.
For more information see [Plugins](https://github.com/laurent22/joplin/blob/dev/readme/plugins.md)
# Searching
Joplin implements the SQLite Full Text Search (FTS4) extension. It means the content of all the notes is indexed in real time and search queries return results very fast. Both [Simple FTS Queries](https://www.sqlite.org/fts3.html#simple_fts_queries) and [Full-Text Index Queries](https://www.sqlite.org/fts3.html#full_text_index_queries) are supported. See below for the list of supported queries:
One caveat of SQLite FTS is that it does not support languages which do not use Latin word boundaries (spaces, tabs, punctuation). To solve this issue, Joplin has a custom search mode, that does not use FTS, but still has all of its features (multi term search, filters, etc.). One of its drawbacks is that it can get slow on larger note collections. Also, the sorting of the results will be less accurate, as the ranking algorithm (BM25) is, for now, only implemented for FTS. Finally, in this mode there are no restrictions on using the `*` wildcard (`swim*`, `*swim` and `ast*rix` all work). This search mode is currently enabled if one of the following languages are detected:
- Chinese
- Japanese
- Korean
- Thai
## Supported queries
Search type | Description | Example
------------|-------------|---------
Single word | Returns all the notes that contain this term. | For example, searching for `cat` will return all the notes that contain this exact word. Note: it will not return the notes that contain the substring - thus, for "cat", notes that contain "cataclysmic" or "prevaricate" will **not** be returned.
Multiple word | Returns all the notes that contain **all** these words, but not necessarily next to each other. | `dog cat` - will return any notes that contain the words "dog" and "cat" anywhere in the note, no necessarily in that order nor next to each other. It will **not** return results that contain "dog" or "cat" only.
Phrase | Add double quotes to return the notes that contain exactly this phrase. | `"shopping list"` - will return the notes that contain these **exact terms** next to each other and in this order. It will **not** return for example a note that contains "going shopping with my list".
Prefix | Add a wildcard to return all the notes that contain a term with a specified prefix. | `swim*` - will return all the notes that contain eg. "swim", but also "swimming", "swimsuit", etc. IMPORTANT: The wildcard **can only be at the end** - it will be ignored at the beginning of a word (eg. `*swim`) and will be treated as a literal asterisk in the middle of a word (eg. `ast*rix`)
Switch to basic search | One drawback of Full Text Search is that it ignores most non-alphabetical characters. However in some cases you might want to search for this too. To do that, you can use basic search. You switch to this mode by prefixing your search with a slash `/`. This won't provide the benefits of FTS but it will allow searching exactly for what you need. Note that it can also be much slower, even extremely slow, depending on your query. | `/"- [ ]"` - will return all the notes that contain unchecked checkboxes.
## Search filters
You can also use search filters to further restrict the search.
| Operator | Description | Example |
| --- | --- | --- |
|**-**|If placed before a text term, it excludes the notes that contain that term. You can also place it before a filter to negate it. |`-spam` searches for all notes without the word `spam`.<br>`office -trash` searches for all notes with the word `office` and without the word `trash`.|
|**any:**|Return notes that satisfy any/all of the required conditions. `any:0` is the default, which means all conditions must be satisfied.|`any:1 cat dog` will return notes that have the word `cat` or `dog`.<br>`any:0 cat dog` will return notes with both the words `cat` and `dog`. |
| **title:** <br> **body:**|Restrict your search to just the title or the body field.|`title:"hello world"` searches for notes whose title contains `hello` and `world`.<br>`title:hello -body:world` searches for notes whose title contains `hello` and body does not contain `world`.
| **tag:** |Restrict the search to the notes with the specified tags.|`tag:office` searches for all notes having tag office.<br>`tag:office tag:important` searches for all notes having both office and important tags.<br>`tag:office -tag:spam` searches for notes having tag `office` which do not have tag `spam`.<br>`any:1 tag:office tag:spam` searches for notes having tag `office` or tag `spam`.<br>`tag:be*ful` does a search with wildcards.<br>`tag:*` returns all notes with tags.<br>`-tag:*` returns all notes without tags.|
| **notebook:** | Restrict the search to the specified notebook(s). |`notebook:books` limits the search scope within `books` and all its subnotebooks.<br>`notebook:wheel*time` does a wildcard search.|
| **created:** <br> **updated:** <br> **due:**| Searches for notes created/updated on dates specified using YYYYMMDD format. You can also search relative to the current day, week, month, or year. | `created:20201218` will return notes created on or after December 18, 2020.<br>`-updated:20201218` will return notes updated before December 18, 2020.<br>`created:20200118 -created:20201215` will return notes created between January 18, 2020, and before December 15, 2020.<br>`created:202001 -created:202003` will return notes created on or after January and before March 2020.<br>`updated:1997 -updated:2020` will return all notes updated between the years 1997 and 2019.<br>`created:day-2` searches for all notes created in the past two days.<br>`updated:year-0` searches all notes updated in the current year.<br>`-due:day+7` will return all todos which are due or will be due in the next seven days.<br>`-due:day-5` searches all todos that are overdue for more than 5 days.|
| **type:** |Restrict the search to either notes or todos. | `type:note` to return all notes<br>`type:todo` to return all todos |
| **iscompleted:** | Restrict the search to either completed or uncompleted todos. | `iscompleted:1` to return all completed todos<br>`iscompleted:0` to return all uncompleted todos|
|**latitude:** <br> **longitude:** <br> **altitude:**|Filter by location|`latitude:40 -latitude:50` to return notes with latitude >= 40 and < 50 |
|**resource:**|Filter by attachment MIME type|`resource:image/jpeg` to return notes with a jpeg attachment.<br>`-resource:application/pdf` to return notes without a pdf attachment.<br>`resource:image/*` to return notes with any images.|
|**sourceurl:**|Filter by source URL|`sourceurl:https://www.google.com`<br>`sourceurl:*joplinapp.org` to perform a wildcard search.|
|**id:**|Filter by note ID|`id:9cbc1b4f242043a9b8a50627508bccd5` return a note with the specified id |
Note: In the CLI client you have to escape the query using `--` when using negated filters.
Eg. `:search -- "-tag:tag1"`.
The filters are implicitly connected by and/or connectives depending on the following rules:
- By default, all filters are connected by "AND".
- To override this default behaviour, use the `any` filter, in which case the search terms will be connected by "OR" instead.
- There's an exception for the `notebook` filters which are connected by "OR". The reason being that no note can be in multiple notebooks at once.
Incorrect search filters are interpreted as a phrase search, e.g. misspelled `nootebook:Example` or non-existing `https://joplinapp.org`.
## Search order
Notes are sorted by "relevance". Currently it means the notes that contain the requested terms the most times are on top. For queries with multiple terms, it also matters how close to each other the terms are. This is a bit experimental so if you notice a search query that returns unexpected results, please report it in the forum, providing as many details as possible to replicate the issue.
# Goto Anything
In the desktop application, press <kbd>Ctrl+P</kbd> or <kbd>Cmd+P</kbd> and type a note title or part of its content to jump to it. Or type <kbd>#</kbd> followed by a tag name, or <kbd>@</kbd> followed by a notebook name.
# Multiple profile support
To create a new profile, open File > Switch profile and select Create new profile, enter the profile name and press OK. The app will automatically switch to this new profile, which you can now configure.
To switch back to the previous profile, again open File > Switch profile and select Default.
Note that profiles all share certain settings, such as language, font size, theme, etc. This is done so that you don't have reconfigure every details when switching profiles. Other settings such as sync configuration is per profile.
The feature is available on desktop only for now, and should be ported to mobile relatively soon.
# Donations
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/readme/donate.md) for information on how to support the development of Joplin.
# Community
Name | Description
@@ -516,69 +60,7 @@ Name | Description
# Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/dev/CONTRIBUTING.md
# Localisation
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/dev/packages/tools/locales/joplin.pot).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file.
- Once it is done, please [open a pull request](https://github.com/laurent22/joplin/pulls) and add the file to it.
This translation will apply to the three applications - desktop, mobile and terminal.
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from the table below.
Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<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) | 77%
<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 | 22%
<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) | 56%
<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) | | 44%
<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) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 86%
<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) | 98%
<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) | Fejby | 96%
<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) | ERYpTION | 96%
<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) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
<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) | | 43%
<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) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 95%
<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 | 25%
<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) | mrkaato0 | 96%
<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 | 100%
<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) | 28%
<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) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
<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) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
<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) | [Magyari Balázs](mailto:balmag@gmail.com) | 75%
<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) | | 76%
<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) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
<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) | 85%
<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) | 53%
<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) | [X3NO](mailto:X3NO@disroot.org) | 88%
<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) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
<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) | 70%
<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) | 49%
<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) | 78%
<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) | 97%
<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) | | 35%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
<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) | 97%
<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) | 98%
<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) | 97%
<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) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
<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) | | 63%
<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) | [qx100](mailto:ztymaxwell@gmail.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) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
<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) | 88%
<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) | 86%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md
# Contributors

23
crowdin.yml Normal file
View File

@@ -0,0 +1,23 @@
project_id: '624298'
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
- source: /readme/**/*
translation: /readme/i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
ignore:
- /readme/_i18n
- /readme/i18n
- /readme/about/changelog
- /readme/about/stats.md
- /readme/api
- /readme/dev
- /readme/news
- /readme/cla.md
- /readme/connection_check.md
- /readme/privacy.md
- /**/*.yml
- /**/*.json
- /**/*.png
- /**/*.jpg

View File

@@ -1,9 +1 @@
<strong>Joplin</strong> is a note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="#markdown">Markdown format</a>.
Notes exported from Evernote and other applications <a href="https://joplinapp.org/help/#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be securely <a href="https://joplinapp.org/help/#synchronisation">synchronised</a> using <a href="https://joplinapp.org/help/#encryption">end-to-end encryption</a> with various cloud services including Nextcloud, Dropbox, OneDrive and <a href="https://joplinapp.org/plans/">Joplin Cloud</a>.
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
The application is available for Windows, Linux, macOS, Android and iOS. A <a href="https://joplinapp.org/clipper/">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.
<strong>Joplin</strong> is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="https://joplinapp.org/help/apps/markdown">Markdown format</a>.</p><p>Notes exported from Evernote <a href="https://joplinapp.org/help/apps/import_export">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.</p><p>Joplin is "offline first", which means you always have all your data on your phone or computer. This ensures that your notes are always accessible, whether you have an internet connection or not.</p><p>The notes can be securely <a href="https://joplinapp.org/help/apps/sync">synchronised</a> using <a href="https://joplinapp.org/help/apps/sync/e2ee">end-to-end encryption</a> with various cloud services including Nextcloud, Dropbox, OneDrive and <a href="https://joplinapp.org/plans/" target="_blank" rel="noopener noreferrer">Joplin Cloud</a>.</p><p>Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.</p><p>The application is available for Windows, Linux, macOS, Android and iOS. A <a href="https://joplinapp.org/help/apps/clipper">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/" target="_blank" rel="noopener noreferrer">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB" target="_blank" rel="noopener noreferrer">Chrome</a>.</p><div class="top-screenshot"><img loading="lazy" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/home-top-img.png" class="img_node_modules-@docusaurus-theme-classic-lib-theme-MDXComponents-Img-styles-module" style="max-width: 100%; max-height: 35em;">

View File

@@ -182,6 +182,11 @@
"docs/images/flags": true,
"lerna-debug.log": true,
"node_modules/": true,
"packages/doc-builder/build": true,
"packages/doc-builder/help": true,
"packages/doc-builder/news": true,
"packages/doc-builder/i18n": true,
"readme/i18n": true,
"packages/app-cli/**/*.*~": true,
"packages/app-cli/**/*.mo": true,
"packages/app-cli/**/build/": true,

View File

@@ -12,31 +12,34 @@
"node": ">=16"
},
"scripts": {
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
"buildCommandIndex": "node packages/tools/gulp/tasks/buildCommandIndexRun.js",
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
"buildPluginDoc": "cd packages/generate-plugin-doc && yarn run buildPluginDoc_",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
"buildServerDocker": "node packages/tools/buildServerDocker.js",
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
"buildTranslations": "node packages/tools/build-translation.js",
"buildWebsite": "node ./packages/tools/website/processDocs.js --env prod && node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
"buildWebsiteTranslations": "node packages/tools/website/buildTranslations.js",
"buildWebsite": "node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
"checkLibPaths": "node ./packages/tools/checkLibPaths.js",
"checkIgnoredFiles": "node ./packages/tools/checkIgnoredFiles.js",
"checkLibPaths": "node ./packages/tools/checkLibPaths.js",
"circularDependencyCheck": "madge --warning --circular --extensions js ./",
"clean": "npm run clean --workspaces --if-present && node packages/tools/clean && yarn cache clean",
"crowdin": "crowdin",
"crowdinDownload": "crowdin download",
"crowdinUpload": "crowdin upload",
"cspell": "cspell",
"dependencyTree": "madge",
"generateDatabaseTypes": "node packages/tools/generate-database-types",
"linkChecker": "linkchecker https://joplinapp.org",
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
"postinstall": "gulp build",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
"releaseAndroidClean": "node packages/tools/release-android.js",
@@ -47,18 +50,18 @@
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
"releasePluginRepoCli": "node packages/tools/release-plugin-repo-cli.js",
"releaseServer": "node packages/tools/release-server.js",
"cspell": "cspell",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"spellcheck": "node packages/tools/spellcheck.js",
"tagServerLatest": "node packages/tools/tagServerLatest.js",
"buildServerDocker": "node packages/tools/buildServerDocker.js",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"test-ci": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test-ci",
"test": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test",
"tsc": "yarn workspaces foreach --parallel --verbose --interlaced run tsc",
"updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
"watchWebsite": "nodemon --verbose --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
"watchWebsite": "nodemon --delay 1 --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --watch packages/doc-builder/build --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
},
"husky": {
"hooks": {
@@ -66,40 +69,41 @@
}
},
"devDependencies": {
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"cspell": "5.21.2",
"eslint": "8.46.0",
"eslint": "8.49.0",
"eslint-interactive": "10.8.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest": "27.2.3",
"eslint-plugin-jest": "27.4.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"execa": "5.1.1",
"fs-extra": "11.1.1",
"glob": "10.3.3",
"glob": "10.3.10",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "13.2.3",
"lint-staged": "13.3.0",
"madge": "6.1.0",
"npm-package-json-lint": "6.4.0",
"typescript": "5.1.6"
"npm-package-json-lint": "7.0.0",
"typescript": "5.2.2"
},
"dependencies": {
"@types/fs-extra": "11.0.1",
"eslint-plugin-github": "4.9.2",
"@types/fs-extra": "11.0.2",
"eslint-plugin-github": "4.10.0",
"http-server": "14.1.1",
"node-gyp": "9.4.0",
"nodemon": "2.0.22"
"nodemon": "3.0.1"
},
"packageManager": "yarn@3.6.3",
"packageManager": "yarn@3.6.4",
"resolutions": {
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
"eslint": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"eslint": "patch:eslint@8.49.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch"
}

View File

@@ -50,10 +50,17 @@ class Command extends BaseCommand {
{
type: BaseModel.TYPE_TAG,
},
{
type: BaseModel.TYPE_REVISION,
},
];
const lines = [];
lines.push('---');
lines.push('sidebar_position: 2');
lines.push('---');
lines.push('');
lines.push('# Joplin Data API');
lines.push('');
lines.push('This API is available when the clipper server is running. It provides access to the notes, notebooks, tags and other Joplin object via a REST API. Plugins can also access this API even when the clipper server is not running.');
@@ -72,30 +79,30 @@ class Command extends BaseCommand {
lines.push('```');
lines.push('');
lines.push('# Authorisation');
lines.push('## Authorisation');
lines.push('');
lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.');
lines.push('');
lines.push('This would be an example of valid cURL call using a token:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123');
lines.push('```shell\ncurl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123\n```');
lines.push('');
lines.push('In the documentation below, the token will not be specified every time however you will need to include it.');
lines.push('');
lines.push('If needed you may also [request the token programmatically](https://github.com/laurent22/joplin/blob/dev/readme/spec/clipper_auth.md)');
lines.push('If needed you may also [request the token programmatically](https://github.com/laurent22/joplin/blob/dev/readme/dev/spec/clipper_auth.md)');
lines.push('');
lines.push('# Using the API');
lines.push('## Using the API');
lines.push('');
lines.push('All the calls, unless noted otherwise, receives and send **JSON data**. For example to create a new note:');
lines.push('');
lines.push('\tcurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://localhost:41184/notes');
lines.push('```shell\ncurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://localhost:41184/notes\n```');
lines.push('');
lines.push('In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.');
lines.push('');
lines.push('For example, for the endpoint `DELETE /tags/:id/notes/:note_id`, to remove the tag with ID "ABCD1234" from the note with ID "EFGH789", you would run for example:');
lines.push('');
lines.push('\tcurl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789');
lines.push('```shell\ncurl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789\n```');
lines.push('');
lines.push('The four verbs supported by the API are the following ones:');
lines.push('');
@@ -105,20 +112,20 @@ class Command extends BaseCommand {
lines.push('* **DELETE**: To delete items.');
lines.push('');
lines.push('# Filtering data');
lines.push('## Filtering data');
lines.push('');
lines.push('You can change the fields that will be returned by the API using the `fields=` query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude');
lines.push('```shell\ncurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude\n```');
lines.push('');
lines.push('To get the IDs only of all the tags:');
lines.push('');
lines.push('\tcurl http://localhost:41184/tags?fields=id');
lines.push('```shell\ncurl http://localhost:41184/tags?fields=id\n```');
lines.push('');
lines.push('By default API results will contain the following fields: **id**, **parent_id**, **title**');
lines.push('');
lines.push('# Pagination');
lines.push('## Pagination');
lines.push('');
lines.push('All API calls that return multiple results will be paginated and will return the following structure:');
lines.push('');
@@ -131,15 +138,15 @@ class Command extends BaseCommand {
lines.push('');
lines.push('The following call for example will initiate a request to fetch all the notes, 10 at a time, and sorted by "updated_time" ascending:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes?order_by=updated_time&order_dir=ASC&limit=10');
lines.push('```shell\ncurl http://localhost:41184/notes?order_by=updated_time&order_dir=ASC&limit=10\n```');
lines.push('');
lines.push('This will return a result like this');
lines.push('');
lines.push('\t{ "items": [ /* 10 notes */ ], "has_more": true }');
lines.push('```json\n{ "items": [ /* 10 notes */ ], "has_more": true }\n```');
lines.push('');
lines.push('Then you will resume fetching the results using this query:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes?order_by=updated_time&order_dir=ASC&limit=10&page=2');
lines.push('```shell\ncurl http://localhost:41184/notes?order_by=updated_time&order_dir=ASC&limit=10&page=2\n```');
lines.push('');
lines.push('Eventually you will get some results that do not contain an "has_more" paramater, at which point you will have retrieved all the results');
lines.push('');
@@ -161,24 +168,24 @@ async function fetchAllNotes() {
lines.push('```');
lines.push('');
lines.push('# Error handling');
lines.push('## Error handling');
lines.push('');
lines.push('In case of an error, an HTTP status code >= 400 will be returned along with a JSON object that provides more info about the error. The JSON object is in the format `{ "error": "description of error" }`.');
lines.push('');
lines.push('# About the property types');
lines.push('## About the property types');
lines.push('');
lines.push('* Text is UTF-8.');
lines.push('* All date/time are Unix timestamps in milliseconds.');
lines.push('* Booleans are integer values 0 or 1.');
lines.push('');
lines.push('# Testing if the service is available');
lines.push('## Testing if the service is available');
lines.push('');
lines.push('Call **GET /ping** to check if the service is available. It should return "JoplinClipperServer" if it works.');
lines.push('');
lines.push('# Searching');
lines.push('## Searching');
lines.push('');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/help/#searching');
lines.push('');
@@ -189,7 +196,7 @@ async function fetchAllNotes() {
lines.push('To retrieve all the tags that start with `project-`: **GET /search?query=project-*&type=tag**');
lines.push('');
lines.push('# Item type IDs');
lines.push('## Item type IDs');
lines.push('');
lines.push('Item type IDs might be refered to in certain object you will retrieve from the API. This is the correspondance between name and ID:');
lines.push('');
@@ -237,7 +244,7 @@ async function fetchAllNotes() {
// });
}
lines.push(`# ${toTitleCase(tableName)}`);
lines.push(`## ${toTitleCase(tableName)}`);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
@@ -245,12 +252,12 @@ async function fetchAllNotes() {
lines.push('');
}
lines.push('## Properties');
lines.push('### Properties');
lines.push('');
lines.push(this.createPropertiesTable(tableFields));
lines.push('');
lines.push(`## GET /${tableName}`);
lines.push(`### GET /${tableName}`);
lines.push('');
lines.push(`Gets all ${tableName}`);
lines.push('');
@@ -260,50 +267,50 @@ async function fetchAllNotes() {
lines.push('');
}
lines.push(`## GET /${tableName}/:id`);
lines.push(`### GET /${tableName}/:id`);
lines.push('');
lines.push(`Gets ${singular} with ID :id`);
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## GET /tags/:id/notes');
lines.push('### GET /tags/:id/notes');
lines.push('');
lines.push('Gets all the notes with this tag.');
lines.push('');
}
if (model.type === BaseModel.TYPE_NOTE) {
lines.push('## GET /notes/:id/tags');
lines.push('### GET /notes/:id/tags');
lines.push('');
lines.push('Gets all the tags attached to this note.');
lines.push('');
lines.push('## GET /notes/:id/resources');
lines.push('### GET /notes/:id/resources');
lines.push('');
lines.push('Gets all the resources attached to this note.');
lines.push('');
}
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('## GET /folders/:id/notes');
lines.push('### GET /folders/:id/notes');
lines.push('');
lines.push('Gets all the notes inside this folder.');
lines.push('');
}
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('## GET /resources/:id/file');
lines.push('### GET /resources/:id/file');
lines.push('');
lines.push('Gets the actual file associated with this resource.');
lines.push('');
lines.push('## GET /resources/:id/notes');
lines.push('### GET /resources/:id/notes');
lines.push('');
lines.push('Gets the notes (IDs) associated with a resource.');
lines.push('');
}
lines.push(`## POST /${tableName}`);
lines.push(`### POST /${tableName}`);
lines.push('');
lines.push(`Creates a new ${singular}`);
lines.push('');
@@ -311,17 +318,17 @@ async function fetchAllNotes() {
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('Creating a new resource is special because you also need to upload the file. Unlike other API calls, this one must have the "multipart/form-data" Content-Type. The file data must be passed to the "data" form field, and the other properties to the "props" form field. An example of a valid call with cURL would be:');
lines.push('');
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
lines.push('```shell\ncurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources\n```');
lines.push('');
lines.push('To **update** the resource content, you can make a PUT request with the same arguments:');
lines.push('');
lines.push('\tcurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('```shell\ncurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696\n```');
lines.push('');
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
lines.push('');
lines.push('Or if you only need to update the resource properties (title, etc.), without changing the content, you can make a regular PUT request:');
lines.push('');
lines.push('\tcurl -X PUT --data \'{"title": "My new title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('```shell\ncurl -X PUT --data \'{"title": "My new title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696\n```');
lines.push('');
lines.push('**From a plugin** the syntax to create a resource is also a bit special:');
lines.push('');
@@ -340,7 +347,7 @@ async function fetchAllNotes() {
}
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## POST /tags/:id/notes');
lines.push('### POST /tags/:id/notes');
lines.push('');
lines.push('Post a note to this endpoint to add the tag to the note. The note data must at least contain an ID property (all other properties will be ignored).');
lines.push('');
@@ -353,25 +360,25 @@ async function fetchAllNotes() {
lines.push('');
lines.push('* Create a note from some Markdown text');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://127.0.0.1:41184/notes');
lines.push('```shell\ncurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://127.0.0.1:41184/notes\n```');
lines.push('');
lines.push('* Create a note from some HTML');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}\' http://127.0.0.1:41184/notes');
lines.push('```shell\ncurl --data \'{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}\' http://127.0.0.1:41184/notes\n```');
lines.push('');
lines.push('* Create a note and attach an image to it:');
lines.push('');
lines.push(' curl --data \'{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}\' http://127.0.0.1:41184/notes');
lines.push('```shell\ncurl --data \'{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}\' http://127.0.0.1:41184/notes\n```');
lines.push('');
lines.push('### Creating a note with a specific ID');
lines.push('#### Creating a note with a specific ID');
lines.push('');
lines.push('When a new note is created, it is automatically assigned a new unique ID so **normally you do not need to set the ID**. However, if for some reason you want to set it, you can supply it as the `id` property. It needs to be a **32 characters long string** in hexadecimal. **Make sure it is unique**, for example by generating it using whatever GUID function is available in your programming language.');
lines.push('');
lines.push(' curl --data \'{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}\' http://127.0.0.1:41184/notes');
lines.push('```shell\ncurl --data \'{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}\' http://127.0.0.1:41184/notes\n```');
lines.push('');
}
lines.push(`## PUT /${tableName}/:id`);
lines.push(`### PUT /${tableName}/:id`);
lines.push('');
lines.push(`Sets the properties of the ${singular} with ID :id`);
lines.push('');
@@ -381,13 +388,13 @@ async function fetchAllNotes() {
lines.push('');
}
lines.push(`## DELETE /${tableName}/:id`);
lines.push(`### DELETE /${tableName}/:id`);
lines.push('');
lines.push(`Deletes the ${singular} with ID :id`);
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## DELETE /tags/:id/notes/:note_id');
lines.push('### DELETE /tags/:id/notes/:note_id');
lines.push('');
lines.push('Remove the tag from the note.');
lines.push('');
@@ -397,23 +404,23 @@ async function fetchAllNotes() {
{
const tableFields = reg.db().tableFields('item_changes', { includeDescription: true });
lines.push('# Events');
lines.push('## Events');
lines.push('');
lines.push('This end point can be used to retrieve the latest note changes. Currently only note changes are tracked.');
lines.push('');
lines.push('## Properties');
lines.push('### Properties');
lines.push('');
lines.push(this.createPropertiesTable(tableFields));
lines.push('');
lines.push('## GET /events');
lines.push('### GET /events');
lines.push('');
lines.push('Returns a paginated list of recent events. A `cursor` property should be provided, which tells from what point in time the events should be returned. The API will return a `cursor` property, to tell from where to resume retrieving events, as well as an `has_more` (tells if more changes can be retrieved) and `items` property, which will contain the list of events. Events are kept for up to 90 days.');
lines.push('');
lines.push('If no `cursor` property is provided, the API will respond with the latest change ID. That can be used to retrieve future events later on.');
lines.push('');
lines.push('The results are paginated so will need to may multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
lines.push('The results are paginated so you may need multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
lines.push('');
lines.push('## GET /events/:id');
lines.push('### GET /events/:id');
lines.push('');
lines.push('Returns the event with the given ID.');
}

View File

@@ -33,7 +33,7 @@ class Command extends BaseCommand {
const stdoutWidth = app().commandStdoutMaxWidth();
if (args.command === 'shortcuts' || args.command === 'keymap') {
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplinapp.org/terminal/#shortcuts'));
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplinapp.org/help/apps/terminal#shortcuts'));
this.stdout('');
if (

View File

@@ -52,7 +52,7 @@ export default class PluginRunner extends BasePluginRunner {
this.activeSandboxCalls_[callId] = true;
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
promise.finally(() => {
void promise.finally(() => {
delete this.activeSandboxCalls_[callId];
});
return promise;

View File

@@ -57,13 +57,13 @@
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
"sharp": "0.32.5",
"sprintf-js": "1.1.2",
"sharp": "0.32.6",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
"strip-ansi": "6.0.1",
"tcp-port-used": "1.0.2",
"terminal-kit": "3.0.0",
"terminal-kit": "3.0.1",
"tkwidgets": "0.5.27",
"url-parse": "1.5.10",
"word-wrap": "1.2.5",
@@ -71,13 +71,13 @@
},
"devDependencies": {
"@joplin/tools": "~2.13",
"@types/fs-extra": "11.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.12",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.5",
"@types/node": "18.17.19",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.6.4",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.1.6"
"typescript": "5.2.2"
}
}

View File

@@ -66,8 +66,10 @@ describe('MdToHtml', () => {
actualHtml,
'--------------------------------- Raw:',
actualHtml.split('\n'),
'--------------------------------- Expected:',
'--------------------------------- Expected (Lines)',
expectedHtml.split('\n'),
'--------------------------------- Expected (Text)',
expectedHtml,
'--------------------------------------------',
'',
];

View File

@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
<en-export export-date="20230724T173816Z" application="Evernote" version="10.58.8">
<note>
<title>Boomwhackers - Rio</title>
<created>20230412T112328Z</created>
<updated>20230524T133616Z</updated>
<note-attributes>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><br/></div><en-media hash="b687cb1ab2d8f3f10b95c3a500841c66" type="application/zip" /><div><br/></div></en-note> ]]>
</content>
<resource>
<data encoding="base64">
UEsDBBQAAAgIAGV8uFYt16suHCcAAMUCAQAPAAAAc2NvcmVfc3R5bGUubXNzpX1bc9vIeu37/hUuVyp1UpWReSdV8Shlasbjqdhjl+XtPfN0CiYhCTFJMCBoW/n1QV/Q/V1WQ4D2iy2ub61GA1jd6BsaL//zx3737FtenYry8PPz8cXo+bP8sCm3xeHu5+d///T6p9Xz/7z628v9+ZTfbMoqj9zZxWj0/Opvz569vKkfdrn5q/n7mN3l/yi29f3V
6mKyfPki/o7xN3lxd19fjccXi0vH8EikfKiKQ5192Xnt8mK0WnmuCEXNr9/yw9v8tn6XVXfF4Wp0Mb8czedjpxLBqHq/3SZFPMaP9Kk8Jg8UY1yzLuu63CdlLMxymDoYCzFFx6FkNOo+fS9PxTbfXnlm+O0opzq7vf378ZhX67La5tVVc38VRqhvy++KSjFCfZNnDfK6LOu8+pBtjf1MJpIxIv2laP47bPKrxcXcKwLkaMXhVFfnfX6o/8j2+fvb
21Nem9Qh7iTZ16/lrjksT1yhjrwvDjcPpzrfB3xl2Br29OyHwMeGrVCfk11x59P5VPo71uQdwU6QH0zh+JxXdbHJdjfHqrl8RgFxfx3tD5fa1cReRoow0v+csypfV9nmq7mIF5PAZQEquT5Xu4eoGLcKhosr6bPHryPLc7heHp1OyDXkzEbvsuiZbaIUjIlSdDJyiULmK+gRFAiSD02hel3s2qu/sHQBOvLuoSo2pw+7bJMbg5obKCFGLE+vvpTf
8mc/fn7eVOEPPz//afL8haSs8135PVKmgvGuMDVXdGV7SIELiatLospchFRQSAM+uphQFeS/LQ7hydFyCUapv2Sn+yaZt/nhrnlCjC5mLZ8HlCT7ESQrJgkBIIlnvhAaeBom9rqs6PWNECW+MiW8Ka6n/I/z/ktTXQa+isjL9Om+2Hw95KdTcyLsWsUAuxP5rjjtM5vs1a554P37l+zUYIc83BPKANKmWqaHIqg8dccczem5I17yNHRQSv86lqeP
WV2UjYzdkhigkuZx+Lo81K+bYnX1qdjnp2d/5N+ffSz32aEVUwqQ3hT/m5vWzGw5nguJDQmJyf3NMdv4ZxzE0VGOTdbP+1/yY37YsiohRUCJ2KbaSCpJAy4ErstdWT2rbGVxZ//9Yv/Nfn4+mc9F1dHwnTk2zYHzShkoxGWequbJ++nhKPIUUERv2wAj8/DBISRzzUXiJB5Ako/l+bDVOXMwEry+G3rVrGwdZYZkhPb/L/7/zCTBpaa9+JhtGQeJ
kXFZTIqgdWUAHilp3iQDJsPty2EpGHQrjKDTwZGgMqY9zGEoAC5WMShUPhYRKBJOFjiUDPRy0A0y821xd67y7To7nZxT98Xu4eqd7Wqur1++wHGl/cs33hdM8Rdr0pMAb0rggBLZm28bXiOmibiSBLsqzLePbWPVXrXQVjAdJQD7tA0mugmNAMGh7fsuz07NwX2n3LZ8GeSIX7IqGKshhZ8uui3PTd9hzTkCbHsfW0Kbz02/Y7tOpEbaf9MlSVCc
hEtBkAXomFVTe2R1g5uS+0tZ3+THrDJP/INXdRBECp+K48ncOg6EfmfVJnLTFFR3kwGq6e/Ou7o47nLfu9W4vx2uW9Rex1lzHRnEWOTKEKLsR/pOidOPaXeWJtli4oITtr7q2+Jb0fTQzVCJuQ70pyLcPOyvTt6pFmR8E1WSP0Wif2rKX4LyF6d8tCU6Uj6SEk6hdOZCWIv+lAn/CUh/SVKbw03T3mbjT8vmUgvQMb/mD6fijo9VvXyhUH8b91+K
+nyiTA45Wm0aDiLRxfTlC40z/s0m2+XPvv/8fPz82b3590WobrbXTd7/K3+wJxmHK3AgXgIRsP0vgDMB7ahKKBI/uSxLsoTDNQZ8gAZ6W5VwukRpde+Gs0LIDbegiBaZXNyQXEy4UoZpAp+qrNh571FXJKPxCsqTsToUYB6BqkTMCc0Qc91kpMnDH2Wdv95ld7YGhnhbs+Z70h6KP334vqzq/HBjBrFMmPyMet+zn7rRw/BbEm722W7X3K0JozmU
HM0kfmPq9bflxj1X2gPrCM3kyca9GygSx7DMyKZplb8uq1fnujTR09XMDWXBmE/fZNERbMVW5ZuQr0TIP13yxgcf7eOPVASNDAdCRs1N4rddgqHJwdDxxdQ2PDD11WZTmH5BtiNpL+ZWAWJOd2iSos8r+9iToM+4axOF3o3NN8cY0Z2/G4D5UJ6ufhoRhQx6ZeWQm7wqcmttgXBa06KuHv70bTV3p3EEpf6Por7/kFXNRbnPTw1nLA8mCS6RsmmJ
0FO4uS+/j01OccC3SstzpYK//qhNR66y5/kIg84ZNPVCKNHtDEAEfY8j3941D3pKNQM9ApXcMLY3nVKyKu83OzMmVZ4KWxYmrrhzUJBf2cbebESor1j7z4NsQKutSETEP7+R2ydNXkBAKkT5M8dJBFkrMt/GYhQ7p+NlbFEihj+46QPF4KltCkKcNmdgqZ62zZpkuXbhP7L6XDHlrFXKUFujZeFxYU4r/AwPoOZnvm7g9sETf8cU3ACym8kgvyPh
j/JmV7rhAPrTm+GQHa/Pp7rc23Q/lb9VhZ2fwYG2x9TUsnbIxf8VYF3ZSjBQPzaPFOELiQZu0zUS1awAHfNYNSdW1aHdZdoh7jbACBbZxxwUkQegDGFBa0czx3Xe2aesv24S0kQ9hRKHKSjv1WHTPJ9/yW+zpstmqod0MCV+e26aMU3paSripgjNUBKckkrofVN5V7agJUK+DsyaxoQbVCh2u7fFvqhdMUMBJ7nPiupYHNgElMI4tWsKKnLEHJSi
mCoZJDVN8ERyM0lrh3kuzAgnxxjvujzUHrelSMOMHp47k0ClNYmHwuDrr9vvxSEwxZgsQd2A7IgxyUAsOXUyCgtQnTAYf+0K6wTssNWEq8hQVrhevYYMPftT/qOGs04qzvNDB1wVpqlsqBUFtIR0KjSs6WF4VYOa3HtglYoGjaqSy3fFLqa4WU3jcWPhjfnrIro+BBj/l3wTAttiH/k0oI9gypAN/T93mH/jxwlhdKyoNQf8N3lErLUlwXrzVO6K
LS8h2rPtjKJ5iE/jEQiq6L9lR832oMpKPOy2YeU8P4k8GZjmYMFEyby1QZ+VS6VieTzm26YhR2t4gVCarpLjsE/LkHX7xZxTZONegIx5qtktFCih0osx80R9gQLsrwBl6ovypiy/+mr/J7vGRaKEKyt5Dgqiq+AnhEWq93A5SOWuMJkgqNjTQSlu5ycEQmg9qyrLTVbnIkpzQatygUgaq8Y1LOmkCpegpIbqW0KS2LvqjpJBFXddNc2wWP6aHAmE
0nSJHJEC13KS7a1vRdOtq0t2NIVxalcdEDniiGPyWKr25eHhdeUmJ1wbhENhEOmNC6hxJIl7QfbDB+jAztQurwKBILpuGsrbm/vitrYnduWWe0kU0O05KrpD2cmyy6swR63MpP0f531eieoYB/zQVlNxfWtusV8DxI6TjLGsfS6LTfP8qU3q5F4wWAlsAWRc2oBy9+bcTrkFXoD8qLK9Yg/7LyVyMX1scKZcv4aJa5BmktiRJLv63dkU1I5E5a3p
Tlez+12CV/KhBGNYFvogMKA14oGVCiUOBh5fj3ISSbUPMxzQop5VOZXAp5smgAzS5xwOJETsqZeMJsTkGZiIJYThiZiIJGS9n49KOegxSctwh9PXaaevU05fp52+Tjt93en0dQ+nY04iqbYvjgNaNPymrB9z+jrh9HXK6esOp687nb7udvq6w+nrLqevk05fdzl9/WSnr4c7nT5Ygpuvs/0xOxQZbyAIs8tQ6HnAgNYIs6dCiYMBsz/KSSTVVus4
oEU97wuVQLNrAsggNTsOJETM7MloQkzMnoglhMHsiUhC1tvsSjnI7KK1I2v2VDgpDpZPxaBSGL8jmj4wsH8fWjrBthAkY1Da864JFSwNkIPzS8tEMpaWspLRRUgnQcpHOpyWh1KSDqbFvcsKEg9rBWXHMsxImwcY/e0ot02/ukneTHhNzDLR8IuFzQqFUQjH9QoG+MuukLV/RJC/ujN3BP3ejkXd9Fv7ZwywDisHIummNtNfJ7uul/yMBDNwcLry
OXA/6KnV7bHNnzFgFn7ausEF25/yuG05H10s6fF58W8PHMmrmBtNfV8VZkK7vWcSCkMNVi0HJgSoyXRYQoCOfLovv5v3zeIrRQJBtPcHt75VgX6sK2LbrVkG3r4+ysGYsl8JwvPAwQSZ5ETh/mpQ+Hezkv9btruKK3NEAIj8S49jIWHvQrLIK7+Gyq3owRF/Pma9lZmcN0XBuoohjnRXNeWAkRhCSObFz7iyyq/+RRGm+a1NzYumQcMjJMv2JHie
A0RoZoWnY608q0V8BvKDAcylbf8Mgf+yyzZ8yP+IqvJsFsS1qzDbBASq6CxNASoyzRmF/Pk1LZA79+7Ywhgw/iTxvx8KM98W/3Yxu34xO2yzamsurnnX2K7HgniQ/JZXpi0VAm49ikQD/fV5t8MSFIkZK3e3+d1dUUoVCMRjVflhc6+OI1BHz8510yasi811dizqbFf8b1gVmQo54c68KH7dtD+a50pZ2TFWeySIC4l5z8JkhAsi6rPW+DM7nth5
KIx24tx8VL1t+4qyt336c797XZBBqAAQ0i/5aVMVR3OmNuSI/79J9uLHfueFkkT0dtXcKS5s0SAiv9r+9/lUh4xJnEjeldvitsgrmjzFAFUkLmAvKJt2QVV/KOrNvWXS355S5VntXoQIawVPtb0tqVCYLvh1f6wfwoLIiZ0q4FigvntntK4N6FbPUsTT9gZRizsFqrnvstPXN+vMPtQg7iXiRNqltabFm4jRY5mU6FLCZXswHpCSz03zpfzK3qqd
UKWKJxLwU6BIy2ZHy50rIvCOpoP0qO9bVvYj3MjL9rgoiMSkZTaXWrFgptjm1jVNvfzNVQgScsStWRbURBz4e1NIq3YhlVuklw67BNxmDL/bbp+t9d7LNB5h+BZlRAjzc7Y757ZR3BFu68Dv2cPJrMb1GzGc/nGfH8gJv6ry5kwarasde7Nl8mx7iJ4H6amJ9+53tqHH77dNszBCdorrUZJvFzS1jWlWXI1HTY7CLxdsqs7myfUbowjMNwrqbLPJ
CDx3b4lx0HN354onyRBHelUdwzOZd8BghIvgUlsiTS62TRPe2bdIzHLqbg5Ph60CWUWtXAjS4mbZhVmfYZuVEhRU64jfDzf19vfbT9kX46dkLF74Xw/bNkNmZwIGRda7IrAmY8cKUGQ1nck635IRCAmGB9Gnol17bl+QIr/pMUVPW4Le/Pb9n5sm1iRC30XCAZ++e9vjQ9Y03sxcOfvdPqhOdrMYO3xtBpuu3ua7pslqHlMywhRmsQfhPzO/gygE
Yy/PvcDU9gX9L3putu4zvU9bKQuIEkkflAO+4m3+dMm/vfqXY1PNkt+ScX1F49cq/pHFP7aPve22TbB5zMUfInpNo9cy+tFmjvyMl8pthNReKv/LPw7sj3hdzPiGgCiRXCoOxBN1yb/1J9r+kvHrq3+5poxrxfjIUiCXKh6A/BBRlzz5KeIfqbpN+1u5q7POhcSWYeoR81bXyxfxJwnLFWsCFEy2Yk2ghEpXp809Ua9YC7BfnkaZbMWaReVgOQcF
0e/7QFhk/CvmOw6CK0wmCAa800EptteHKeQV6zmkarlw+JpECNXtD2DNcbHwSxYnPDU2mi0QSWMj1xqWdPKMkKCkhhFpCUli79HnKBk05tw8x7Jv2erzKzZ8C1BOX7NlTQCl9PH8HUqew0KADsBhKphM4BE4LAToCBwWV8iuS/7XXf0fp4f9v97V/+ECr5rLbsAXHiUXjyxkDphZ4FwczvngxJhQ3AycmBkeSaS2Rql0Z60rtWTezC2Wyf3PuThs
i01+KvbJ8w06mVgqj30T7cqpuow00eTpB6FMrU9WO1NN5tXYWqbaPIrrYnvuvABBJxNLZbVvol05VVeVJpo8/yCUqfXJameqHeXo1aHE3k8USU9XiXQXpGRiXSV8WM7WMGfrp+VMymSR1FmjDk8VRJBDB/cpNulEO8v3k3KKrqWD/4mcdl1TU6B0Tqm/U8UQ5NTBfQpNOtHO0v2knKJr6uB/Iqdd17TzVcZASa6sdwzTlXDztKG9ECFJdHO0PzEm
nbd1qOyNSFRy+VtHEqds8a6RBiXZ90AYl/VKHOyGv0/vD7uHeBkoSMmyDyNQSQ3LNwWkrkLsx2hQJQp6Mh1RJW8X6kiIEns20R05vDbjjGP7NXV5bI8gwlDpjCR6RJDDTof2fCSkiKzvA3AlIL0fhSpy6P8oTFF794CIZtg7OtkXOzE8bXLe/u0jVb5v0ml32JqYt3cIwkjr8kd43di8Xq9gxqYFP5JlwfcwGTBcBa7cpMehbtYmmBaggO4GKf2G
ZYV5//xiMRFixWHpmFHqm2IbzDG2mxEkgkz5/lzTGL0aIuRkxutZ1eQpN/OMbm9iBjnapipPJz+JZKdH/KyjQh39a/5wE/aYsFSB+Dyfjzuz7OmH3w7CZpZjlPj+fPv+1q6rMJWOwij1sxkWFFsrgQCT3NDdAamEBajkxm5zdFsjlYpJod1LKqXkQSo1d58fs9WpiJSJI1Jd+nhrvtFeuPBrvdueC8RdksIR5MZJ/rj2KddWoAoDmeBcClKyfEoK
VFLtI/GS0shD0oHiIalBlSZ4SHZElbx9f0FClNizFndktnOr+69NnQ76sQtL3uZ1u+6loqyMxhFDsUWIPxf60JSQIrKHJsCVQNlUPTQJHB6aClPU3g9Nohn00PySVVUen1dju20XRfz8htlY0O8OZ+tVDoS07Eo1Os+lMEfdF4f2gwfqTVIV8M2C6pCZ4bzYkmOAI5mlS0cz8PfGv/7/0HDMLuPsII+yXGJbD7MBRg0KMngXl7wLSGgdL+W3LDXL
iALizNvLZ8vXx+xwZ0t0V9g7qGlx2r1b2FvOChTkrjePCSn56nHL4eVTgYDMyyiKABEtpxoHglhWNQro/csrVQ0qsX5jSHS3UiEo7Lpzipq8f5zJ7mIqlBSyO9oRTyZAJ/dT0aQ43OlkLCntfdeBduA2BK4zkvE99mIjn0Wc5r/P+2PnuMk+q77iF55jfeT2zZItHAkrchgJkBhjimYOQHXCoKHTFdYJtE8RhTFqz/vq2WSG044EMJhngRYUhWkq
KxoooCWkMGhY04P9NajJvQ1PRYOc7oXpZh0j2KsWT5SAjOx3VNtld+RECcjIfpMm+ssXwqLe5aqxz0BBtJY3m0RyhLBkS19iMkHp7xFNO9HMD/EwqsARQuv7EDNc1sS3ticw4XW00GM4tAAERE+BtRQ4Imm8jaBgSaetAwFKamwXCEgS+7cIgmRYc+D85RMypMI13dXOM84lzmxhYU4Eg8SBRTvjIInWqBrk5L5PX0/XjuURzpa+Hetrz70LUHFu
rJWkQEDmLSMUASLaGtI4EMQWkEYBvX+rh6qGvUlZ7o/lSb9xrHBNj3tISJBz5d4RAAaJA3N3xkESYVcUBXJy3+0MPN1ZuDIDJP/+xX7kLR6CbQ3hsXS1zBlh5wiNipNj+0woEJD5/hIoAkR0XwmNA0HcT0KjgN5/HwmqGv7BqKYXL92tcE0P7lYg5wp3IxgkDtzdGQdJsO9BMZCTh3zKqKHTNYTe3DzE6Wlzcwb7SBRHxblRc2sQkPXnpGQEiOSn
pDgOBPwzUhwF9GGfkGpVAz8g5fdIlu7WASCwVl4JMn0XvMWFwSGO0gf9x24CSiR+RUqhgt7zcgc+HDcXUZkjak2AIjozJw4hGbEnCiBJMCiCkaC3RbnsaR7tqCfufzsXdVa9LhJ2ThM6EmjtnQ4mxLI+74p3HR999a8XsSvRUOenowl539pI6mhjpy0lCVIq46xCT0e75LyK76R0JUMr/Q5CVxLxMdAR7kqg/4MByoc1f2QSsRTaDVN5QaweK4hp
QkcCbUFMBxNiuQvX4wURHwLtxzWgIGJuO43bEU3I+24nhcuYeF4lWKmc05LYEe2S8827epREzKHbeD1aEjEjbuj1aEnEjP5be/3zJVElkX4enuxOO3gfMBjDsrbcQVxL5AhUIpQ4Fihjj3ISSYURKRjQor4jJEQCi5ImgAyGIjIRGZRDSzLGh5dS0YSYfeYNxhLCONyEIwlZ/2EnqRw2rkrUHc3D8nAXX1JXbUMcTUljpx6HkE42CJPB5EFRU/Bx
VjK50AhMhJCwb5uDiWDbDzCQtvcNZa3DRCgp5O3CdDyZAG0RpqJJcWwLpmJJaf9WoNYOK2Tmg4rp8pMKJ8WhBKViUCmfLulo+sDoGdODlk4wPGlSMSjtWzVyFSpKkALVHY0FkXc275GKpaX8MdVBSCdBH1bJcFoeH1nJYFrc/8EFxIOK1THrKlWJaEoaZgkTIaST30pJBpMHBUPQPVjJ5ML3VBIhJOz7RZFMl5O4BARFkS5diESWaRlKhZJC/qWW
dDyZAP12SyqaFMevuaRiSWn/77to7bB1J36BpVrqJHFNj4udJMi5crkTgEHiaMFTVxwk0Q4LaJCT+67t8XTWYYnvMfGwyA5b/qRAQOYLoFAEiOgSKI0DQVwEpVFA778MiqoG+TH/cazyk9kRUDoSRJAkuBLAki+ciQPwIMCdjzBgMq1DESwFPS99FMD9R2RYitjabwRLgazI6bJuclq0ACAYClghSMSgkBQEGIGiUBggDiW9C4TQDVsEm++PpVp/
x0BBDPvNc4Sw5Po7ickE0Xs2yaAUt9vqCITQei8ib7jQ1iRCqGSpY3t4tfrRouIdAIZQWtea4ZYhP4zECeoNB4XSq0dLjUAkTbweIGFJZy8GcFBSySsBHJLEAS8DtJLhBeH63rzDwZaDt1dRhqCKv4oOg0onXkpPRKAsfgATB5xon9dVeSj3alWjDgBBKO4aFWxR7CGODgB6J90ElEjbJwGooPe0UeCz4gtQQZd9FhEQ7HRPJZ4ILacARXRWXnEI
yUi5RQEkCeUXwUjQuxxz2aCyzLan165HwYSwnVrBASBShSARSx0PFobHSKnE2hZgIgJkve8N/SyB8DB9G4byut58Agm2ryGkQkD4WZRVHALCN2nhm04hbC8ABrpDvFzDSEomyncqnJKzco6DKSkp7ziUEg4o91I6rOzbna9v5Lcz3MuZo3Zn7EScJmER8c56IqJkqs4BISgK9Q2AsQBVGI9QcEKhskC4kjzlZnbuU9q/qoiZEBUFDCjRZ9YLxQEl
epMS6eohxhJjOYqh7werGhCOJbxaSASxlFYJMIRlsTqAASzqXxUI4cDXO7PDaZfVpWoBgAiShMEeAEu+7PLCADwIevmsmwGTiZvbaFgK+nakgkC2aWVE8jveTovZY91PAEMB74jiGBTSLimKQFHsnCIcSvp3U7nuCW+tqwlejkpqnM7lEOW1L1PTCVwYUImjKdt0VMnDBK2AZOYGvRoOG2c0RMn8WaIwSgXbBODtATqHdAKl42PXjqO3x1cwu6Bs
ZlhAigj2CUDTvzGg9gXQk70RFvsA6KndCA987/8pi5Cy21u9zI6CghiLDEMICxUYhMuE4Sq6VFCK47o5hohs9V7Y1fBxSYmR4QXFaHk54QildW6e4Rkd+7tYii4jEqVXkZUQjkiaWM4nYUlnC/g4KKlkyR6HJHHAIr1WMmzFan6fZ9Up273Lqq9qJTgMJoRheQOOAJVc+p2KpQ6Ilnw/Skol1s4lJCJA1ndZMdUkugWAg/IZjKryKSysg2w/wXQ4
JQ/WXiA1W8WtonEFdyKUEvZfua2kTy8DrOJKRJCsq38rmaJCm2GiqtaS0VZ8zLPabCCoy7GKIIkrwSvJZ8W3DaiyiwLwILDUdjJgMuGj6QCWgt42agX4c+kiLEXCORpW50EfRQiGAv5qBo5BIX0ZA0WgiBRegEPJgGLLdAPLrNHaXS+x23kIisJHYhCuFNDxKoIPlPR8moIT4q4XuJIMug9WwTY9EM4nBKUD3he4Ph/tfoFjCfC/DmKpKgEyhGWi
DMgAFg0sBVE47MVbeyhRADgoiKFfwxHCkm+CS0wmCEax0kEpDm9+c4TQ+r69bLhysIqAhJUeonK5oMYUiKTx974VLOn0fW8BSmp8z1tAktj//e4gGbg4wm+ypxYKCVzT4yiqBDlXLRrSMEgcLh3qiIMkwsipAjl54F6UYc95Z8WwXa8KcxnfVQ2gaLgOjwSqaEoalngkQkldctCvk5VMjg8C6hASPmmvyMS9SZHgCClb/ZWMOendrjidssNWLbTT
ASBo5wQ1KMiiCEEcpQ9uYzcBJdJOHwJU0HvessCXFboICHa6Yo85o5U7QBGdVfI4hGSkskcBJAmVPoKRoHflz2WDHgDMS3RtHAgIhd3s0f66IHSyBeSXxlWyYDCM09riwH5HiigEEhJpAesnY0La1lYciKSeN8VQYVczBiIxbW6bC+prDggSc7NCBZl4WGCCGJwrEEHr7degGLYJu7/j1KUcIxe+qsrv5JMjAnNE/x1e4VCBSmroVwqI8oRVNagS
Bc3sjqiSt+ObEqLEnvfGkfXmjxSnzLRzfWaodyWkiMy/AFcC4mGFKnLwscIUtbeXiWZY58592Vj27jgqqe3XMARCabKDp0CVJuripaNKHjp5AqLEvp0YS+bD6H7/Ohqi5I7unssP6+8JSBF5j0/jSkD7fBJV5Njrk5ii9u/3Rc0g+xXxBc57tLAtGU/LQ4cwGcRa4diucMfBwQO/F68jybZWTQexuOetkzLYWMAknEC6LEgmG9FLBxPirsUPgNyx
DEKy1axIFyFx52iBTwc7xKwS6GR0JEIqho54RwKhsuiIdsh7VyBQPXD5RbH5WuitxhSu6WQdhgA5V63E0DBIHK7F6IiDJOKKDAlycu95fEdPLMugQS7o2mrKMdg6Vg0Kcuf6pUhKfiO05YDVGTogritfoyFBQBYrNUAEiNh6DYUDAVm1oVBAH7B2g6gGbrx5d67yrfnAcyhD7242ZZWvr80WoToKdXGXWA0rgdopFkbwceBusd0UnFDcMRbgStLz
PpxPeTX+o7kHVy9fxL9JTNZfHBTEUHNxhLDElVSYTBBcvXRQitsrJhBCG3KV5MAcAQkrXSeRcLuiRUL0BGiFIBBJY1WBhiWdVAISlNRQ/CUkib0LfpQMKvVGNiE+nQifTpBPJ8CnE+XTCfDpBPh0gn066fIpDkox9ekE+XQy4OJOkE8n0qeTbp9OtE8n0KcT5dMJ8ukE+3SS8OkE+XQCfTrRPp1An06G+3TyNJ9OiU+nwqdT5NMp8OlU+XQKfDoF
Pp1in067fIqDUkx9OkU+nQ64uFPk06n06bTbp1Pt0yn06VT5dIp8OsU+nSZ8OkU+nUKfTrVPp9Cn0+E+nT7NpzPi05nw6Qz5dAZ8OlM+nQGfzoBPZ9insy6f4qAUU5/OkE9nAy7uDPl0Jn066/bpTPt0Bn06Uz6dIZ/OsE9nCZ/OkE9n0Kcz7dMZ9OlsuE9nT/PpnPh0Lnw6Rz6dA5/OlU/nwKdz4NM59um8y6c4KMXUp3Pk0/mAiztHPp1Ln867
fTrXPp1Dn86VT+fIp3Ps03nCp3Pk0zn06Vz7dA59Oh/u0/nTfLogPl0Iny6QTxfApwvl0wXw6QL4dIF9uujyKQ5KMfXpAvl0MeDiLpBPF9Kni26fLrRPF9CnC+XTBfLpAvt0kfDpAvl0AX260D5dQJ8uhvt08TSfLolPl8KnS+TTJfDpUvl0CXy6BD5dYp8uu3yKg1JMfbpEPl0OuLhL5NOl9Omy26dL7dMl9OlS+XSJfLrEPl0mfLpEPl1Cny61
T5fQp8vhPl0+zacr4tOV8OkK+XQFfLpSPl0Bn66AT1fYp6sun+KgFFOfrpBPVwMu7gr5dCV9uur26Ur7dAV9ulI+XSGfrrBPVwmfrpBPV9CnK+3TFfTparhPV0/z6SXx6aXw6SXy6SXw6aXy6SXw6SXw6SX26WWXT3FQiqlPL5FPLwdc3Evk00vp08tun15qn15Cn14qn14in15in14mfHqJfHoJfXqpfXoJfXo53KeXT/PpeEQH+kdypH8Eh/pH
wKsO5IP9I+DW8QgN948S4/2jzgF/HFVyNuQ/Qp4dj4aMVI/gqP9IDfuPHhn3H4GB/xEe+R/pof8R8q5H9eD/KOFeH5DD/yPoXw/zCYARdLCHh00BjJ7oYTZZpWar8HQVnK8CE1ZwxgpOWaXmrLonrR6dtQLTVnjeatDEFZ650lNXj81docmrxOwVmL7C81epCazkDBaewkrMYaFJrMQs1lOmsZ44jzWmE1ljOZM1hlNZYzSXNdaTWWM0mzVG01nj
xHzWuHNCKxFVcuZhOKc1HjKpNYazWmM1rTV+ZF5rDCa2xnhma6yntsZwbmucmNwap2a3xnB6a4znt8ZggmuMZ7jGT5jiGj9hjmuX1x/ByiYJK3L8RpXAGFN+lUqjOmH0HaqOsE4gfHlKYoza98tGjp147UpFmehNWX59k5s3YxtnLAKbwIzO1jcpjFPV6iZ1R/XaJkVhrxrw28O+KUXuGtskWgUYn24BPQ9kvTE0Cfi9nzmbbQhNLnhzJ+pn5tts
7DYIP9BSrzBN5V/qAgEtod/mUrCmx69xKVCT+39/i4ieUvx/PWyNK+1VmYa8UdTRj9lu/+5cq7XUCtf0UGEokHPVJ4M0DBIHlUZnHCQRPw0kQU7u/QEaR0/UHDrMZbLuADgXsNpDg4KsV0fOVM67dnqht4VWIRoHd5dVIjrCFaIaQbAWxIoEoPr+XH24eHfB74l0CK1INAjI4hNLIAJE7LNKCgcC8iklhQL6gM8nEdXAb445pahSEOwEt3m1z+oM
WHJE1tFHlnyqaRJfsjt7+QLgXlD4D92y1wUA6uhZVReb866pS8qDOkgqKI6k1hPDiBPdZ0V1LMShli9fAJxV50oAcCco6zr7lim+hvnNVQIU8JJ8m+00X6KO7DYTATtHSdjR2z0GVPIo4CT8dXclTIf9Eatit1P5U6gjfyu+VFnNPmLSFGyAenq54x41ZIm1bgprg4GfYMzn/3xsvKBPQMH+au3Oldlv/FP25abYH93LQhpU5Otyvy8PguxBVlRV
4hBHkngIiDtJ+70tdRgcgKJ4IBxgRVUdCOJIEg8DcV8BbUz7QR0EwUAQD4FgfxPrJtbYU991GIAicvdhoD39qvlRbMxucOCypYJJMb2AqSCrJdVBIY4k8VAQ53WlOgwOQFE8EA74WjM73Z3zbKtvWiKCZfFgiYiT7ctqi0wIcSSJx4F4W1FVujBpUJFj4hp05O/ZvUpYYZIak1WYo96Vu6O+1QDV9Jg2QB19c99cKfMO1af7YvP1kJ9OTb09GjX9
AhDxJf9cl0fTfvn1kH3Z5Vtb7CXmK7z8Njvv6tPnvDJfYruajZpGkAQd1XekrsYXy9nlcvXyRQu48IvQgm9u7vmU21d5rv72f1BLAwQUAAAICABlfLhWalpDxJYNAABz5QEAFwAAAEJvb213aGFja2VycyAtIFJpby5tc2N47V1bb9s4Gn2fXyH4afchUew4N0DRoG02iwLToJuks8C+yTLtCJVFry5N3F8/pKwLRXvkuJJt0jp9SUl9ong5Ojzf
Z4q0fn+b+cYPEkYeDW57/dOznkECl469YHrb+/Z8f3Ld+93+zZolEXlyaUhK2+Hp2VnP/s0wrHlIp6Ez+3N5xWYXTgeWKeUKho/kh5fmjUfkyr0cF7bFBW6cPo7/j/3/D2dBwmdnanjj2x6rYuyw2o3JxEn8uGdbZn49M3eTMCRBnObaZ5ZZSS9N7rzsUcNrZlCklhejF/r6OeBZI5/YfcusZpRG34J56AWxI5iJWaXhPWsdiXKbLFVe/uKEUy+I
eF3F5NKAzknA70z/LrNmJHZ4dwSsnNueE4ZOMCUh74nsylo7l87mNHqP3XwRetOXeKNhSJyYddudE5OePTgbnJ+cDU/6g/q7fFa460UbS5/RH2TGhu0hmY02Vzq3fvZin2wypqx5XuD49zScOawisze//oa578QTZtyzv3huSCM6iY3/esGYvkYbbqRkY0MjmoTuxjrHbJAjVg26sSteafj9fZ3GLbMOS4KY/2cs3/DVCeP0tev3ljn81YydyUTK
zLOfF3NiTEOazFnjvdh9IWPBhlnxJ9tRPH7gfc+6PU2LBmZRjlD2KHTc7yQ2YpbNH2tEc4eR0KBnuNTnnGCKxk74hxeQJ2bCXxwx+Zv0mDVtGuyrTVINYt7EB37nV88JqGWWGbnJ5yCKw4TDPK0p48xgfDLn1mKdfRpMxXKKdGnCWCaMlzYBPU1ZJ0uXNlJ9jH8M/rmmThxTXvCV98pXe8B6u0wJFs5bltc/u2YmRXK1kA+VQj6sKeRDtRDRxCv6
5/PY/k4WI+qE49O0g07TzrLMikl5p+uTiRHx8UgBcM8mDZYj9hibEX3yQGNyt2DD67lLtl7NLW/5EMaem/gpRVbQ8IP41PXiBWsIK6NIiSZTRqnPHuvmmwvLLBIVSK0vvvLUnGBix3VZRhR5M9r75aqcn7dWlQa1uDhrXos5Q3ujSlxeNa9ETIKkSR1Sk6aVmDlh3XAM9tITEZtaf9bU4mI/fZFX42kDRjdXpw2MZiOzqTKbR6jFyjzXQnZzVbYf
pk8vThAQv2KbeQvGD8dPiDT3cyqnQRxS3yeh4cYhEwd95jFktpfnknG0YLrHvvcTj80Ny0SlWivPt8xyEi7mZZOrpEwwrRNH1p8f6ZtQxgvh8pp1h2Vm/y2vPZO3uFrFeMF8iVSdsSqmCfFyzO0/Ujp7feESKYyME+PR4/JBLsisFv13T4qSUf3DPoSh8S+mLmPjf6+s3e94kim23/pCnCgJK3IkZuU8kjlzJ0Qd94N6bvX5nyqTcprFhttldeFX
Un31bzZ1S1nVG1IhzRwhNnWLN63Lrt5oyk+3OIyfvKlkF3nTB3vIxQH7u3LpLrt0J5e+prBMfkrdy18B33FTd8cesRfulXnQRUbVcMJEib+wedmR8UBejUc6cwLLzPLl2v1khH56c3Vxw6v4U+6AEfXHqaDmf6VO5XW0JuzVM/iN7E1Li+EOCM/MLk1YJW97UmVKm5HNFMbIXgVUqaBlpD0T5tOu1IXn9U8v+b8rXtqqzYQRBH1Ni+vzxxepqhnT
oXfMVXUYnuyTm5ubVJkWOVVbOplEzEN542GU4fnF+VXPWNz2Toanw+uzocQ7RY9Fi5nNHC6uIP+TsJeAhN/mnIhmtnFrpHS5ri9WWs14kobymIxpnIYc0r/SpSRMmTZF+f+XD2aGYm71hifWiXdeSNyUn5M5Hw8xp2rNm1PN4pjlat2+ZH24/J98PZ67dp8PF/srXzNXS+TcLLW5c6/LcNvXZR1MPhJn9oWOic19xSJRg5d0xnrZAi5j+hr8MmCu
+rWAuQFgtgDMoA3AHJA8rmqxcAEsbIGF/rZY4NzQaGRNuYR6PhqRqRe0Rkl1sBrWwuoasDooxTQc3F1yhim5KJYpOTdrnJ1Vr6ZrENla5Ss2C0HCHnAWauLpvDj+ZE8Y2eWUAtY5HOvAp84ffaQwgU8Nnxo+tQGfGj41fOrU+PCwat9hgk99ZBBpx48S0fD6QvmPwL+IkEEtQi6BkCMgkf3qlF0iqmtY2NrJgU6ptgaw0iL2f1Y7uEMMLuYPzB9K
qM29YuGi3n9pFjCFOgW7iP0DdoE6hTpVAFZQp0c8uJg/8jxgAeo0z4M6PdDU8ZzMfSLbBunn4XycIq5xxGTV0HHjJL/CXn0xKbXcidKvj0uAFjkS3NL9AFZAk31bllZ2zfdeZRdermt8iro1BVvmavMVe8fAtwbmXohssa3tYOFCcyxgNQx4AbwgtlVV5xt6oTDUDAua+2c7jduQYLwUz9V9Kn7FbWNFLfdU4C9fmYBft0tWarRqSjnP/90bczyS
SO7BrV9QUy6kc3DUnRiP5zsUYUDXj5rJ1e1BB6rZFyD1eqYjvz2UW7acnfbT7VoGpxd/s1MLXm7F5ibt1ctoHajMDazTyudpB2Weeq9aJ0+q7Uli4zKGzR2v6kIGzDeGau99wwE9ntf4MAOqQqC8GZFfIjzaCAEqhMfVdSI6KQnhZBxNBAFYABZyY8wv6s0vmiHgKNhAXb2po9p4R/hhqPOAX9fvUoQJoPUJ4JgDUPVqolMLu44iAAV2MJoNqPYB
qOtz7GLXcQdhl6SuoySEm6G2mwE0FJ0DNOiGBkVkI7xQwRB8cER8AMWhxhpurIgSDDVDQ/s7MGsc88LvJyrTDBZgZcaHH1z9IyHwU6BS90P8UKlqTB9YjFMYAgvAQm6oGRa0j4/ByTDADPhGEIgSb9ADUV3DAk4nwzZflbaqSjENBxec0d7g6h8Zw1mnB5xBFFMTqp1biC26dNuiSzkX591bdHUNODjvvXoNUxrOe4cGUnFCOh4N1DUsIKKCzbIr
bVWVYhQOlyGur2tcBhLWUAIL7URl3unpvDj+5AjOXwDrwKfOxgqEVLYVPnXFHj61oanghU9dGGqGBfjU8KkrbVVJusCnPmKItP/rdrOfGnd55hgQogaJ4BS7wlAzLGzt5ECnVFsDWGkR+9/laYddG1zMH3kesKD5WkrlzqqEOgW75HlgF6hTqNNKW6FOhbpicDF/5E0DFqBO86ZBnSo0dTwnc5/ItgENZ47PxyniGkdMVg0dN07yK+zVF5NSy52I
pONeALTIkeCWzEYkXAFNFC98YsdpZVnXpqkV0PG2Xa5rfIq6NQVb5mrzFXvHwLcG5l6IbLGt7WBBhZM5sBrGUAIL4IU8D1jQfZUc9AL8s33EbUgwXopns5KNDRo0YaVGq6aU8/zfvUHDI4nkHtz6BTXlQjoHR92JEd+hqExN2FxVMNQMDWCGPA8h7eNxznH00NL48GjYerbB0UPSNQBLi0+XwBpGe4Or/wEAOHoIHst+iB8qFX6P2D+IiAALef8A
C5hryh6Ck6EjM6irQ6E81HBc94yp6/pNnjDfID72iyGUeiGDNXI6x8fAGkZ7g6t9fOz6HBsFwmfZC/FDpcLzqfYQIvBAQ9lDQMPhpSU8WMFQMzSAG8o8qA8oWqwGKdt6IIY55hgZft5RmWawhiwzPvzgaq9S4bMYUKn7IX6oVDWmD6wbKgyBBWAhN9QMC9r/OgcnwwAz4AtLIEq8QQ9EdQ0LOBgOO6xV2qoqxTQcXHBGe4Orf2QMx8wecAZRTE2o
dmQkdkfTbXe0A7s4yzYvk0soGN74tjfobeUEffLJRKqNSwOXhDG/knbIvWXKWdLwhk4QzWnkBVPxpnXZcrulp1t89J+86Qr0pg88PJL+Xbl0l126k0tfU1jn3hnFzmEdXiAsUDEGQsoO1gUhne+Arr0i7W8i3wgh59c4zLpiDISUHQyEACG5ORDSbYS8OP5kP+GUncKpa0BofyHA/oAwrD+MAF9FwMFT37/ZdHrA5vavnh0A7KoR8t2SChv94HRR
T4aYFQ9LY3uFQv3aOMyL0NtbyaxdTqIAQtE53QYCeAWaRegeaBZolqx7oFmOZQ3eHqeq+hghPg8BEPBNKjQLNAs0CzTLEQEK7rUBZgEQijzwCjQLNEvWVmgWaBYFAaXYaj3MPsohRPPvHyFsjfawoMLOPaqu2esaFsALeR6wAF7I86A6wC5gl7ytYBeojryt4AXwQt5W8AJUh4qIArvkeWAXsEueByyAF/I8YAG8kOdBdaj6G9xeEYUtEQzMNJhp
xLaCF8ALeVvBC1jXoSKiwC55HtgF7JLnAQvghTwPWAAv5HlQHWAXsEveVrALVEfeVvACeCFvK3gBqkNFROn8QS6oxWgPCNhx2gCvqDxTHfvXk+ruOL2pZu84oHW1al17IVrZqqA4CzmgrR2MrMuGBkAhUFjaA4WioWYobEUcqI7CXe7oARQChaW93vvKAMuqhHF2j2VVY4fAIDCYWwODoiEwCAwCg8BgPmbHEk0FkhEtQrQIKGwyNEBhh1CIaBFi
lkCh9iiEusyMD4/lVs6vUttP2uUpV8AgMJhbA4OiITAIDAKDwGA+ZrqcOAkkI1pU2iNaJBoChUAhUIhokThqiFkChU2GRhcUQl1mxofHMtZ2gE+BQWAQGAQGgUFgUGMMQlVmxodHMqJFiBYBhUAhUIhoEWKWQOERoBC7ZqmxG5/Ou2Yt27xMsoRLl9csc5ZEJEv+BVBLAwQUAAAICABlfLhWXvAS6DxNAABbTwAAGAAAAFRodW1ibmFpbHMvdGh1
bWJuYWlsLnBuZ2ycBVSU0bvuaRGGUClpkW4QYejuRrpBQECkRKRTBmlp6QbpkpJmhBGQBgFphu6Uhrv5n3vXuuucM2u5+AZnxu/b+32f5/fs2Z9hGmryeDjkOCgoKHiKCjJa4GcdCgoqCjYaOPKSzI4EPx46Kxi8R0ERMLn/g/o0QA4c0KIoykhqe6Tvtno6UyI+iIt5L5wtY6kT0qIeMD4zjVM5LpRJeWydGp6gxaBmyaTU565zFINU6nYHByXG
C6x602pKuix/luJWp34ohNPJwkhkNZcx0fSN6QdN2KvOeJqFzw6vkDdTLzPnMqhQlLcO/G4l8LxM2/3mZsZEx24uj/lymS1fjmtg/VWfCSAXG/t2GkOeoLFN85I554+4vYZUuf949ifDu2+SD+tuNYMf29/kYTGMX8rjKf23F3atl/nPtvleXaxn0VBSivhefqfFxn11hBC/dXFz0zZYHMwQxh0WERG5Xo7gio6ONv3h9hBe5SNGEyVHarPyBiOr
9rbabVOr1XRhbi6j1wonBA1mcUCKMsmnOsYXhYQOHX+J619ViPvpkp7LTIlIS0t7ClMsGvvCljZQw447eeGTuX6RlEQCs5DIpMWeWi60S7WeNuZkCqeLRkLgRGkNVr2P80YavU63wsIeUiLwsIKH1uOfXlwE7V0t3N0WvYOLdLjMNr1fj5R7Lqv5drJid3+fFGOpL5Ftd3e3wXEuaFuF8f3p1sT0fx5LS0swGCxNwKmG/eqPkVO5Qe3a5em2Y4O3
hwc0bYAX8iQGUWe4W/yq6E+lmf/2Hw6YheN8y+ahB3P41vY23MW4dMK02b28vHw3Xl2syQWJBeeOVfhpU4uHj89pUCNjQw3nJuUy/b1+/O7du/4kjmbPYxvWxHAZul+/1AtGSIKHps8DLfRfvRqrfzeztbUFzuTy6iqZyyyzfxUaLT0pllXZ3Nycl5dXUFDQ2iqaobY/Vf3a1hZbSP7jxwa5PDsnp/29PbWiMdNKAZIIhnjFzHVvb++Zv3/xKaEB
FvpmZlkjtpRUVNYGfxwbcIOGwWN09KgDneD85vbFm8Hs6fKGhrOd6VqtY/f9eV9kD54kYWpqKjcknvyi+PuUZfbdd7yvNIVF94/Q0NBXxeM9URSqjGPDwxqvXsWbVi60+QobM8UrxtGferXK5VmQ9djUlhs3xa1fmDS73+x9bBd2XuxIZNNjDh/ddHv//n2YTG5d3YQtnxxdbGZmZgQpV/337/SPw1b7k9XV1AzZk/TZ15yhfOSq7CR5zDjwqpkG
J/9NZbM+K17z0ULVtPw1zGDJ1EKWcBwMNAic/vFDq6+md4Jv30HgfFkf73gkCb8bvjusg4gMbbv9iEOjROSY4f8Mf6H0SkaTCEMy6Zt5Yo1FlovP3pU83hlRVr+aCdwXvu4kl84VzoQNgS+qSpVOuFK+qTNUtrEikyPECO7MFkn0qT7sphGf90BGLf9k06v4/fv3ydnZ2b9/JAsZqald3GXt7XegqGxT8vN/k1Jn1Z7GXNA9Dls59oySOx6Rp9k8
vQRv2jj1ikGAGgruXJQmEOUTFaWKQbSaOpmZLaysWNESDm+fPnnyRFVV1a9dPEWlvq7OpNF5CTQXvaINb8r8/PzIiCb9a24ysV3WeSOfTkRMTk4OGFzfg84HWMGoJAIWmmwZag3gXT/cFCsn4U6/a9h9NkDd/dw+XXMFHTA4MTERFRnp9vGjgIBAPP1T/NCX5FFqRdK8WWCCBG9n282yL8mjsCqWctPJ4q6PO7O8gTQ8NTu4KIDJLlP/IziHlS29
j/JuvA1ENk47yN3BpJ7ljXwbsTWvNiD81NPT463nhafTKWpGQ0FNzYhtebw2IJTBSR2jrq6eZxQjdrnRytkKPsj0NOZswT9r/SL4AUF47q9jEZhFkzFnfO/4xESn39S6S+3VEOHZ1pW1u/uf/TOKqB7vnBbYFezv2TsgS74Lf/6vYE1s0mBIzja65LGW2J+r5AjdPCfETph5wIWlGHfh0zZrokk0Yof+UoEx/tg3JKgWd4J3MSJK1nfh379/qQM2
GWrJSgVxvSu7Mw3PbrYmSrcnK0O7qxsW/LznfnwYGBhgjH+4twwddEmf3OYofoJfwsfHl51AIH59ACZ4xJaPJOJm5434+yZjUMbg1c4CRDh1379rn+z9XnUNom005mSmS+6fmJxMTliBcpJHgcLOTjBqcAST+YwkQpYu1v9m0n93YwM7FA6m2t5Y6UnYpznH5IonnGTF2qDApx3qu0u7XFrq6nQGLle6ruRBF9A/npubk8vjIcTecBM67wXd9+uX
gro6wfXVvPd+XX09qJWmOcdf38Y2E65CdUtlP8pqftdnN7vZ0qOgoACS2Gnu3GT8/PlzoH+Ygcac66deI+qJAzYDNi+2IrTqIPAAwyDaL3Xd5gZJhXabJsGdziu/YoHmRnQHWXyd3KaiovrQPGd/PQJfcjYxyUAgWTzPwLCZVjZUV484NuhpJyrQR8q9FqwFesNBogz0YGyzrLE3fWjdhbI99OY5dUw8PUs4HcIFVHf7za6T/+VYVlfc5MkJH6Xa
dPXrBzCLKrGkoXXNJwciQy7GTac80UgJlFtiWZeLo1U23bIObvYMNThHMco3qRt0mwrLGAm7nFmThBcS7g1NTU1gmIrGOqsFuSVWKenr+FNUUlUYx/FDpJaAlnGzcxjUrDCJX4TplnIQ4dArXt7cWuqMsi5CObHRvFbIYBdImjvoy5fEUXJRcsj7eXkKwYJZaBVr96p1HLf0KXoG3ry2tLSEw/ACJVAQh6BErn6GPZmsNDOrtQGtY1TvMD1Rqnfq
Jdo45EwpH2clQh0a6GJry0yIDf71B3BufVGk5+3d7f4dH/nAfKt3dbMT069TOaJuqRyUuyCmr3J5gQ+D6HvHitS5jOp/77rn3aIyPY2IWVs79kwtBAXGR+57NvfxbG+udMLeQoQQY+n64tiwnASyCMVP/Oy25ioIKnlo/SmMJ0UFQxJozUvkAwUp/xA8lhM4EU5Ipe/u2w8fPpDBQuBLDvylk9u4IWiShLbXT2RjabFtf0jlZE/YdxqRXD7wgtim
ijkvolAiwuXyNt3+WlZ3PHu16fMpcxS8xuZnF8mseJnuW9ZE3rQB8qi1pVCaaLk8/jQwoO/mWwQTlWLpm7xO7ckegjqDL9na2wf5v3nz5uPOFOqDpGxiqRwNEWpgDs2T2xlpacKeRyvPY0ToMsycKc+8Wm9ubnx9feVjqOtmdvFD4dunaurqRuV/qITdoZT4VmSUH9ky8xXj8pn7tk/nwUfU1r4HDcqnKn33NSMjqttS+OMeBrpahlDdx33TTW78
V1OLi4sdHR1L8NBW08qxzRMPEft03Ay1opld2EXIumftdGQZUDzQ3Th968fZGqmfLcgQLhc+1Legv9SKnlTUGXIMHStEFtpJEi6apRbOOQqcECRg/7KppY6RzxvJ5jR6hLtDc3p6+urVK5EhcJ4O/DMTEyEWZIpx7P1qqqpAXoHR516EIKHBnSjoR0/URVDDwHRpQbDg4LiTG9Jfg0QiOTg4oqKibE9glQ+TrNEDJYB5d1taGCQhoUnWAH7a/9VV
WpAxUm0tYX84/CGWSgRqLMiCbOjYq5WalzyqJ08+BicIBYuPnx+0fZ9bEGpGRgZ+KPrT7MKc/C72/i03oYTYVlxugt5smZ09K6ZEyclRQTVDHGOJXSt3KfcoObjLbGuGmY/YgjyNrxUf+cMg/XY/UUhMGv+7vlVXACXPsbsc+JXXk4059fbeiZNE+B71MsWI33pwJgf7wXrcv0ryO0x96V/FpxL6NHcDQQ3pLgGcwk6iDCby0OQrudIaqCEav/Ol
6teIvqdVLkg5YHeQjr1B8k23VPpeqZzh6WmDihzm8KDQD7a2G3/Kjcwu+vQ0ctYHM7x3qhESFjkav9aPe62OTjbHrs72QwPx4NzZtBK8wEqS+/HAAwKhhDqjYHFrnG7kxeQa6JdOwOBLAGB8AB7E4VdkK+WNzB5fYP4omtw2afMRjEY2GVe0tvpO11g7f1ztS1QYta/j5+NjN6ovmxX2EcsSd1mWOTTwgMAZmZh8xRaA2xYch9Biv3jx4qCHVTZH
g5kIB/TQqNPr4k4iWeL1Lu7I3G9IRExyCybWzlS1NL9FiFQOgBZhymhtVh0zMzNeLtr4XnaTH+Pbp6ZtPhcHS3CguSExEc7QFhckQCaBwrHNeCb18NxG6/4kW76/Y2O1byc5PZJyR5gRLiQRmCdjfLtuQqAOuMzafBY7AsE7gsi+/W3xfBGNdGo9TdIt9V2JldcqHk/iMKL2O7f4Kv84DGPomCTCGlyhUIZaK+luV7uTr1iWbunE9im94nyL53FH
IDrDwmTHkhGggrnAwMAP238UMXUrTfHQyGAAIMqLih7MASrzP/vo/2Byg6SRdPPUK6XEihcQbE/N6fh2ToIl+t+ZGVnNMtsAvuffLMlsbi5P+8o/bbVZ95PYNQGJnmr1FoBZpKgwkkH25ppJf0nRPpKIo0e48KkGvyPWWYLil7CTRKgQq45B4F/owxK+nVTqL14Wj1+9BTNyd6J+Nz4yksP4msdFXYRavnKb908m8MucbUspmhTW4oMWyGe5PFsR
Pe5saF7fY2WbuwEnP3TSFJW1hbubBRqfA8naafb4lLQ079MJvdfVX5V0Dj0otad33VNUgH5erCa/Kk7wCr7z/4wrxU22At0LJhDLZlYyOIqVpxb8sKXNmsjKdABswMNU9ljEf6krxNP6kSwPGQQEGd1uFE0iLtOWX9yR5L0UAo4d+BKCLZdbpauOAKOuz/zvBOx/AAYDU8utA7BcSyc+Pv7j3qxmzRUnCbSdT0Agb+JEdrl54cZR58RHDc/jcrOo
f46XDLJw3iNQkqtUOTmkTNyFtk6aoUYaJkOS1ICV+HXy+kOMNsf1DR91jBF7Umzso/Bc16b0hX35ykd6laZzVRbo12/50wCDt5py7URxUB/c8ILgVXmPIuz9QUw7y48gWGDyUxkKNzftYBYkumeDg2p2LxEIBBW+PH+aSQvrlJl2vEvgDfqf5N82tQik52ZB8u0R193RgMCCiM951dC6coEWuIhpwPMaZ0vQvUOPaz/xv+z69BiSzlDKKvZyaghc
g+jlgvHqomsTaKDU0zEF+uXjiz7fmSFcYB6bbroFOESyxfTNhuZ3AMAAIVXdx6viyW34EjfMIpcZJpVzeXkJYFZfvHvPkU777TWh7KN4ys+tRU0JxCUjMNOTEi063QLWb3o1HbePIrqT2A3QuNZNTEwAPpTqVZJgPItV4Hv50vdkRN45au5DT0pGRjkJcRAtNjQ62PVUoWiMT3VIQE/Qqnbaird47RG4TN3SOtZjQ8b4JWgFq9XLqo+Fcq6BN3ka
zGWGdX3A7JwFeHhcTjY09RPBtfOpfnR377q25CM34dRTVlau63F3PjbouJYacyNeJ21f8L9bT8bUbqEPaNOMWkG7Qhyt9lvMwaynZegOQZgy/yhfHneM+3eSmpIy3+5HaenbTk4d25/R3ZZVBEE+IeMrhi2oWiFb/SYqBmdSiUq9mP9igHw2NQkWjaX0r5bplrrNOtrbo+lhKfgsBj4AsiqWFYo5BySzeQ4/kxeXzZBINqI7ToE+rpe+MbYk5SXQ
URanQyxzMpAQtIoTUuUJ+cdKedsPRNQJZXQKFVIL+5v0+jSEaeqcZfI0p0TPPET6HHOKtcNpQc4B9kW3Tk6JWCrmTlGZSOUkdm4faldRUfHB0uGAgIICXtWBWsn+0K0vy+eoN+i1Gi12EDc3tzOQsSv4hX/7FeKNhWqVoa2DQ1lpaXCUn7Vi1tC6mxBInIsZvjzOzXPZW7dVr8fXkkPyKiudAENNJLbc/U3QFDZrcvYBk6eT/J+nFOIk+NUiC0tB
DPvt/Nb/tf4wwPU/1yhKLhEgdGWhk/ridWvUiSNqyo4eA7fQgKrnjG3ypkT2r2qZmtTrs79sg9sv7TYs/Lu6AYBgnE+tgww98dF3cqp0WM3K/abBLMNGF6vQ8fDSlVbRFTxiEy+6gi2+EoidL16sZzEparlHIBtBW2fdnhSFyVg6OjpaWf3cdU+GLItw/Xbyp/EJjpJ7DTKU0rgtn197O701plS3b2i3kzFLQKHdgaQmYQe3A4/wrb0E4acR24Se
vnxme1MfiU23y64QXOAF3S6Ug27EOCGTZv6+Q5mippVnowXKn72XOx/QYDSokcXBlw6MgNBPAhrdoP+q9M2Qpn/9uNN82KZWMpIyGvmnzCCvJsAZOjs8nD1KkWRtEEcPCEdplU9VXUVl9seH7cXO4NXPHwBr2NRq8X+ytOJ9qklksU4lgqbjJR1HfzEovL/zt065T54B09mvI4YoTMaYk/QyxDZVrUBr9frX89hfIClG04jjz/3QYLblI9ckwg0J
Gv9wP4PArg8RNJPbp9X6kN+gzcij5LYI/dZ+P201bXdaEDtPpv3c0yP3MhMr0Z+fEpE3YgtqN+siUc/0opPAL3t4O5AD2/O5Ublu2gDWAN4jKqJMpApjvAI9YYO+7Bf6i16mrJbz5QiQxcx+uLHALPhUSQnQO2DZzpYZ1gaaRJLm9vZ9NrXTu/+A13u0mjpHxeeAyP1vd+Y+5TyPxf6Uzkq64mxvX9LU9JQv2HBcHIKFLqu5WpaakWFSbYlJnrhE
SA31UagF3nwS+IAapZ62dAKngQwGAp1W9UCagDLBQNHYpoeIacYtDxQ6NTurQogRjWy/nKiMphQMmivYlESxodngY+/vseThM/9Cr0iIcc22O0mJKBizSwlVztEAplLwBn3oOEUFK1cFTfaRB9pCi+crd4dUWuxFm84+kUMMyc+52FQR4KkkGQWFsM85Ny+W6LBh+b/7CPqnPNgolIAGhiD/iRbYMTU1hYwRjy4xYyb6YhPcGWCjlVqYw35+Ntv0
3rSSdEMRQ/Lg6gbAmvTHh0Eoplh2JHbHXcO4DRL6lzVc/ZgYlPbkTa53EpVSALCBkwK8LdZW+nOaDe96Qf8+PuGW7YYNG62FJIBMRfrYdSnZDgt0PchEY7SJLb5X/AgXwLusr7QmAMKlqzHV/AOQBoiVMZ4luX93oX1h/TjscQB1krXTfEuT5/Ga5SqDEdYNwPGxsTFQ3xwsYXNz8WxqMXJ5wLFUGeMJaahhpp85zEax7Ebss/ukab79YGRkJKeg
mHM8q3s7GU0tyrrqnWNHHkUDgeuzkzSKvV+DIFwAVARcFKSTAQ+L6LavC3Iq7tPjlyQEZthrhXbswJ82Yf/OwYFeUasElIG/ldVKGFMmCkUSp8kPz2ObTUoMNEQ0VbAkPqMMFAV2gh1UBU4UYNrrj0OwTg5CuSE6Wc177sw/fo495VAPSgqaMUjMpivRDOwqQ8z38vwQmY1YdRW92qndiJIkxPjFPPE8PHdmF+vdqFscvV6FcRO4VJ1S3c7yZSz2
P4W8896rt6iZrfSKgC0PRPYPPXbchZ0Lnj3dDIBhCQXRFhYW/ttfGOKG+NgmEqHjcauheWddXgDONJ8OMs5ljqOfnp1lnxienGwAScoKXUFJ760EilhtdbN0NS3251oMOLck4TChKiE42Phrxfvs7HV4brj8kVROx7EIHyZ5pLy8/I1fu1oRusUilhRX9+jyF+qfvxhUzVq9TkFxjbSvpg6uuvZ+L77+UjwrDvz8/E+FUQMnEQ4gM+L4Pts9usdh
LWfz3vcF6LI8/SZLljdQwtzD47waQY2+gJkgW4j6iuq5VuVkmgpjAmZmjGXDDlOWj1YSHUeDvoQ9bLyluLi4ra0NzB5+aJbn2lOP93wpKlpC1AYFGrllLNyQ+NZ/fbz2dYaLt/E45iVGhWQ18xqqt9l//vxxE9o99ABh5ODg4G/dW8W4hIo3J6enIEZRi/vNmnNHRkdXGNQW0tOQcpN2GS3t7Oz0xjPF0IgDKsEOgvpe8g0dU1FRyeU9+nqt8UbP
tAEzSCRD7cEDywltMn1X4iQhoG3YHTGfRgN5nXf/1oE8YdbszmH7hRhnPTo/zLFhZvtU97tq6QQITZuhrQ6iDxEuUXK1Hmc6Ei8vcHfdvfnTQh76+fiAEzk5OVFgqj08PCw3auiabfK/u3X1uv3ndEdiIk8XC4LYnz+6/4kQTgttrd5njhHduSO25nUo5QV/JiZYxn/a10mpCNdOlx2VWwPi7XtVOe1Qb1jeHcGgLn7j1i04Y1Pb6yc1rDtSH452
sEdg2VFVwG6maTRxXuDaROma6LfXtP5vZ/q5d+FkmUGt3RhjIzUZmcQmSFxaPJscINZ+vS7T3Y7xO8m7uTrLY8Ag+y2do8Gbov3qgfWvaMl0wfdyhfUrgg/t0luWIzcqyqKnsnRf5WAbKLlUuBYJVJi2srYyDEDNSsezXe+q9Nn7Xscm1CoreTO8z0ucj7NhESvMntwWpVYHqVdaGk5Hk2B5s7u7y0teEbdOT1is1xu54Vhd6syjatBLZ8Wy1Tuw
traWT0Q7Gnlcp0T8UwkbAw28cfVXMxEtdpdPLX38PUh/zvU43dJ5eRSrsCJ+dyyuV6bvsjlaYG+gE81TZtUT/tfL2xt52Kg0Q8Qtdv+o0GrM1dm6QLCKA6t7b6GtFXFYzWnDVkZC4qDTG3k2EONj+i1XJ3FBHTcETXWs05yw2fvxD3rUdzpfeq26lqr6V//Wv5s5P/eQ9YQWvONNAb8i93z5NSXlkaymb+Nz609Sz6KR5nLZzH3m3Mwqf3DQcjSw
qTdK9F++eJF3tFNRgs0IIjs0OiY6+u+uOyXi1MsXYKGO5fwv2nBLzuRXQzLdNWXdls8UR5ecJTjJTOoMd4OyngEvcW0q6YatujYF5NLAqwDilU6UVHEX0H4ZsLE2qomQ5EwuJW/XZicxqrPXEaF2AtTX2jFYj1KbfcV27Vr1+eFMC3HnsUg0suoUx3tVt/SDs/PBznRtKD6V1qKMHhZ377fvqH8+U1BQ9AsSKLzFIO2b4GJC8dZhLe/iZifE9hDJ
dDV9QCSr8+6GiCTINhVQzD8PEae2C0buCeV3srFkCTrWvE8hcCJqtPBc/SSdaRzCElSpR6BVe62+G74zMpoRv4aL//qdRrooT6foamv7C3nILEwRGRXVyWVYAbVzMrK6C9ACBS8Y/dbJSa+KlTyqi9t4kHVz8Bsv6xlBcn/BhL3FF0oaGnG3DY3D3kNt1gKbkycYkgYqRLKUCJ3oXJEhOCPIqHUzu8BaVtO51zBD4UuuTUcXLSO2ZQrNU3//bp16
ERcaNs9Fr5HG0T/jVRa0wESdYnhJBrnwaSsokJ1mlSTc2t4mnhUm6rL7pdHkD9RHkrCjd3wJxJRoy3X5jdPuzWPiPGYc2xXQleVjm8A0eCNQMd4FHkpHodoHUC8XKCcnlZX9Nv9cW1gWRa+IPVI0HIweGKBbytHwBed5YLelwQ83O9BlsqWs9e8n1pIrKipUGIPdHyj9aOm61hqhqM/9yhwOxKdR6RnRlxo1TSLQJW/flvZaGbZwcaIYe2g0vCLB
wiXOlA3upA2nUylbfKWnxbQT8GNuzzWka4+KqOHc/Y8tH2vGu4nMIVjrftxmiNSzDLW0gTVXJ2laEmyMuN4VIIqJb9xSXkeQs9Mqy74Lk5GqnNRL37Ss/gpw13ab9W/hiK1rk7FYVuUvCDqNcJ9jS5WTTrVG3e32amoAfW9bmxglosFxrmRYZfHuQZqOvdfez5+fxs2Obl/b2NgA6CAWEDRI0kGqmsSOyDKsRiCQgP7ww+Ha+GCQDcsbpubwGmbm
Rkfp3vtzsNS7IHuCHxCUGBtdjTfl9SAQEzsBpd9r6fIilPpIy3VLYR/Kw8v1QkbUrRfuetX9jopDj86/0+pauhUzH9jl4KzX6vy7EZ8ZDCyeIrxwcMcvY0Ui3tTvT1Z5jJ8XANKPuvufXz9K6tyB7B9Em69VjOmUOnkgGFJyi5rWf+jR0nN3sg7z9L9f0Xd3c/v2pvURxhIYrOfPnw+JHEcqZdWTiwl/Xbh6cKCsqqpaPBSnwrjguU6VoSbw4sUL
8bES1BQCDIRL33oPyIwDLk8DrPP7egZojl15ynBYY2iCaFeOL9yE3vGRK8alp6RYGNSXlipboxLJgp4J4bA7oC7PbXgvliU/LggSv5arKEuMtmKANJvH5CgfqDPz3xFyefih0ltzx31slSf//vVYVvuI3R1n3bXkjwGBmnYgiY4sdeuUMTfgJ8J5hFFtP8E6DqVEDAwOor2Kk5L7TMeq+pSCAnzIiO048vB+uWvrm83jbyJ+EHx8kPMU6C25ySLl
kBFcbY3grXCTjdNLkFcaFvwoCjSITpsXbvBXYiMl0VILC9BEhoBskUNgIJ2Cc3GmfMOM1l/DrMxHzsGEBaR5NRyrD+b61PooRwMEowQiOLdDdzjxwsXgR1OPpZBkM7/pJeeEMSy1QKzgZ9QxTPGKIbnWnxHIIfG767O2s9biYpbTJhSQAy73V1ZW+rnaRaBQ7DmTojGGeMXPesaPEs2wQSgd2/z0pZar7Xy5M/hBAacKvaJpi4cLqL2Njl9RFAI1
02VqRcFE4bngwM1jY3cXM5SPkFaRF0jI2CZIA8KU+OG5w4Ret9eeMYhagHHV1TxQ10wFepDXgl0tD7hsg7cd+NlEe+MGbEYcHynGFaln3S8Wj5cDN49GdtCwx/eS7WXKocKEV+i7XdIX9kEIhsAVPvTRlguf7zA7ZytKxlkxxufYpSKhHtBOO9i6p0FUOCriXT7m06geJ99toy+PIVhjm993SDUDWG9pzau+fv3qsZET+jgMo7/mV4ZZ0ZjoxUr8
6GZea6vv+mDGhjLCnPtAhEt0dG5+/rYDnUDGkF/r/+0FoL068WrttnSBUpbqVSYJxQI/1GcH7sQfCh8ZHj74/WLoqwpjNLLeaYGTyhx56CFC/YBHAfWd87va6XzZYxGucLp8jGDJ4M4qt02tzbJn4XR06PUzu+7NVBUPg6pmdhXirKd9xAgavgwCmhvaPv13ckL7bvTHjAN/lNwrnsdP7Z/QsqEzGYfCUW+CkVCDKJTR3IJ6VOZw3rSBX5BQeCCy
UelupU9qkbx4chtkKuiwaZuPAeCjfhQsHgxQjrXTRBh9nVa8oqfjr5ST2DUHLfnIs/wu7Qjj778oMvOnasxGkZaWXh/K+thkfFC5sO+/VVyUrlakvQiSi2ODkc3W1X67OA+PRZ3hMx7qmPbLrVJKhOIHObpYDebvPheuCQLUMXdrAu0WBro2Uxsbtv7+4lZ6WOlHUh/ekK0R+J+j99fwZsQqWNnYYJEb2Bv1YoUhkAeVho8zwx/rliYWbqowgjFT
iPs+syuXx5OoJPng07TkD864Xisw6g4FMl5W1OkfRs5RybWKtfnTEsOXoNEeaYsOuNyCoLE3F1PUik69Wvk62KORxdr2NjaMWUPCGWr5xz1RFIB4LnxMC59YYwRK5aDs8Gk8Si1E9PT825vD1ZTlgVCQkTk3z3EmsyUqPcvAkBQ5+vWckJQ0DJWUNB8E8K9mgIl23L2FMnTdETLHuNoIK17JKImc8SvKcNif+L+M7+hqp0GuObrwAazECCyYl3U9
uW+BC3iP+UZfIhvwjwPlnIeYfiYspU9kiAQS/DVjZOioYx7U4afL/K2xjvi9zhffi3H/lTiYJKEMMZflTyO2K55+jo74n61RDkaZPqC1um2OShOIalev1dbWimWpL8mTU1NXlJa+WguRQ9k3DNl87eVlWDpxv08hiv/L3cWQv5iIyHV81rWMoIQHO8nYphuRbOIJgf9+65kK4xjI3QVUdEQ4ghlqLC4Ggt+EaIghWJ2VUOqYj+7ucfRhqRNWvL3p
LWxeQ30YAQ8HwRM/jlD4YanZDcjnTSAu5OWhv/Q1MkrjawYBnAwyGlnj399py5eqwoipGNyJwkusY0Fm8wM3ZLnqQsODIkrOnL9UnyoayWnW5nN9fmgZahBEu3x4vgQPfWTCo2VadxFSuJmURGLOmZ6ZWa/PLioqyrpQO95hxStIiR+DeG6UdOAhUtl+69W90x3x1L3ZBO8F6BZdx+fQs0Le2bpphz+bbjhXBAcjOWIe/CoSXq3zm26s66kZGaLu
O/q9LoPIw+v+9mtlFy/+tHLy9oQT69yvZiV07x/r/pzQHkS/Asaz+jO/wUpbkbWOuAjldYhrUlwYAx6As/gP+KewYhnBaGYAMmqY3xykT7/NiieQurHxMlnIGquutuxbp3NrVbsS6W6BtxK9UEikL2BRY7f76hX0DfPVQgLyc5yujDXFIDrxU1hhUdF95kbxY0lkJsJJ5jKryO16vz6oI6tuZmbGR14ezuqSiaFJe78+/h7EXwGn+bIkPOytiwLt
Q6Z1V8HaafYbWALqlCCxqRLgLFpshpjUQxEu7KCgo4tcw78S6nUQdNGtus4aCt3WLSKKPKzEK8TsbDoCqbmWHBTjPa1cQ1T1VfNINIK+F8yhfbV0YKeCEYgZhTWKBVqr30zMvcsMA8+COhdbBpexgjtVGJ9jS3GTyRBk0mITX3B5ZFb+6iTCD40IK1TOdb0LkKC/8PGzssI4L3ATMnn/vvbEO3XdcxW9M5f5SMRMq8OcG/DcKU8ga5d9Hd099xi8
501xg1IyiFsF/MgzhHU6lZlSlBXDj0T6MPut+5TMfU0MGNMGtkq7uTBpFrxa28rLOZL7w3K/KtAncZqwcqC0jROHaxNK4HaAektU4jBrM05JBbE3f8wOk2cKmsiqN1xYNRX5Z9KCQfsHsx5Ek+gBFlaLqZMgD7PsaqVUXD4wqdMPUXLwYxFDtNgJ7M/q+aWxucx9VvnROFqvx/W1po0/BwKhSeIyc+RPE7/cyAOo9BgD9YqXDKaqrg7iQCKbXhKF
iKTEVugzTMW4qk8ioqIDNQ7S0sFo1Q+Hjm9ubvjTVCVVfqhxGdVyyTHGox7lxTWcePnmaPyyqR0NOY5DqX9TO22sBimvZGOOxXlTa7B5y/EXN+EdF328Yt4Is+9xMLvWQrQMnQV/WkHTeGVl6V//wHDBjMGwsDAQC/sLOVtN2616H4cgcDH6ODgoIFhpA1gRmsQGD6HRIIIk1gRHuvmhDuB0aVS6HkfAl4a3T4FFVTRW6JiZEfhI42DqrKxIcJNB
gmg7bsvhSwdXN7Z3/Mn9ionckNd85AUs7P0kOJh2OI1miu/QQy1fJmnv0WRkZEgkgTFJTU0Nf8uMu6YDsUY9aJAYZO8rMf+sS4KOGkTLyMiosdFsSRvoGpFpi9Qm6g6H6XyngbkFdxJiY5DBNre2tDyC8LDQdwKkSDxOHgZ5IKME6h2m6ThkdTk/UQl5nXwLgujOXWBSU5F9egoLVP/KiuZD/jhMRiij4lv6OmC48h2ydTehQAGBtIEfxhWuQmw4
G6dei9JPZKtzj1kcqpNeG3TtuR4zPA77rzX27204OJiS+u1+rQv7AmQQ82c7C1JGim9RI40PnMzMaCKfaKVYzkCjXwnG+cIG35QuQR5jAGTXY4OEk3+bFehdP/ZqvTV1xkqWZoxvAhZNS3he2n5Z6iqpNbZ5cr8PgDyqRzoUnyPy9AfO+jprT7TGU2MpVT6Ccl22KNPsgGUy0mNiFBCyiqe1I7otWYVD+1wjns0Mj2yfAtA0qnfQnMIP/rV6v/R0
YWocyXoSE179XBsrWaNJ0jmLJaPkbeKlKxuJRcNMDLWo12IH6qYTF+CaS0CEe0kcRmvEBVfqGqd1N0PYrD2N+hLnsSXZ4au35whx03wJWR/YvLj9OSTX3a/9f98F+F9L8Z4HyrJ0sfaCryYOPANZPyY68M8MDwcgG4MeWOCdPaBXZD0H5vZ06o3mFAsGP05idzG5uJCQ0GJlt+Xm7m4IsvjlrP/nXCg/f2HVLADZDqfSdBTVsRwNBd+w4+cY6H4s
eSNTc3McqqHU5bxWOoISvwRCnttW9badf97OMgP2cHl56TozSYkQPehAdzZxhGNiInWMclNPTx34hcXE9lybFOLSMzKsDdwHUl9qBYYHsvQ/i+8FSLWhf+gxa1/HukNPnHy0luakpqoqmzcCg8Gc/K9m6BVr2Jt/KX6h/dzNhvoT6VI5WcoYHclLq6AnKO6NgnCJVZCZViRBxT8qdbGoFPFbhFJHyQlGv2+es6m9KBK/2STwO0CfrmTGVVEc44Nz
2w9uyOVplk7gPH0KqzYsxxEe4ZbK1Qq4thvKFAVZhBKBWkzYXxMBoSsrs/Lxmd119241fXmr9/r16/LycobQCWPOZAV6cgqKCciPTTch8igSYYQzZfLQeqd2kvXDIAnJ3FfFCUHJKpzJ/Va8E5s0wcHBHBwckPsNH+tZ/reHCJoHupBiiIdmnswiXEM6SWkB031ywHBp4qFxkwYLn6r49QFcQ9E8KNsu1dogRO/Fyy+pCf2rHpnJ/67M7i4nx8bG
irVLWltv5MUvY3VLB9aP2fuTTh4FLjZwP5Oje9S+EJP+CyijqedqooeIWtyK+C1SfO9+r0uVLd/W8UW47+xTmORpENKekCqjEJw/nmgkEsqMf7+AGddL7zUqlUPoervqaVo0NsJnUneeSNgen/sNYL7TVNWzx2G/BweV0l47Ozubmy8O50j/ssD/1TkuQyG7JD6UJR5jMQVIrCDPJUfD42zu4/0Ok98k2YTFIZEHXBs5zOGFm7JygrI8ujyQ59hB
SCiLTu+LIRF0Guwl50YNZtD1QbRkMMlBSZGhWnbcOzpRMtiqq2D2eaD/jd9d+81MuwE7ySIzerUx55tq/eEnUmN86dW0io+L6RRrNJg7nGIMCeDc0cjhlz/zZGXXjz3dqCBYRkZGLOFaTU6V4f4qpRNClNHm3FNTU8zhlk5O+yN58iPey79//wZwb2hkNKKOm9lqX+iMLefATVYzvdPme9ViWum29gPEbYTL6ekpZ3Jpe3t7ZeV+NYJa2v704zTq
gs/xgE6pbpMxp+2wbJtQx/ST/E9IqBsIpbKan/yX0h9/weSFBFsxhysVaB1UDti8B8SdT+epHT62iaFLLEmYLzTSCMBJlTF+wCb49Xva53WoPcrJ/S8FBD5ujRdz+V/0f+HERmv8o0Rh4JUxtF661q6mmXD/vSCfgABukIae3oN2DNbFjZG8qSytIan2pEO1RAxImFrR8WZRlpznyNyPHz+eMKl1PGqQoB2emLCu1s8/xoVzLzl/tZ4GmTwaWSLW
rvDAXZvuB2Z+0xctLawjKWlJoaP3WE/JtqyFRuLxNiWoYFb5RxteDg5lIL7W1REPcClgbcaajvGV8/7xWL1dOruywN7d2PiaktLpMrt+nEj/FAslxxw4Zirtfk0PRxsFTsi1n7jqmHw0ZSd9K9PjsHRR73+/Yp/j4eGpixxiuDdUSywdHHDDud/+KdMcmnzOQeQ32dnZKUFb5X/nx8wHXi/8cW9Enka2nUF1DBhwoIT5Qptv3w3y+gJU/d5MAwG+
XJa4/7xltcNMPd4W/hHINetQR9DH3whG+PDeoGq2Awzfb7vam2/1FqWOGaf+TPAwJI7e82ilF4+Cv0PbGww0g3IS7aUps/Jf3ETNJv/v9fWFY5tAd5KSkoAxcxjVf6EvJsRJsTYAAe1QA0KbNnDp0+ZaMQEgHfARiJPdDYWFhX/KjRoc54QMhCQJQbN3mlcVFyu6K+gz8JHj/Wxs9zhc3ptrFhYWXiqwsLe33/1bB9J1/6prkVrGsqff3I8P9/tx
wCuaf/wAOYgW21kh2oLMNislJWVtzUaTSDFOxOOAlvAtMQ5mHP3Z3tzM+DifiIgPOHeAMD/mHOPoCaiEvgM3i5KTJsT4onXpI1Y5iZvz08jQEJwav+Ns44dt3eDOgB0Wx6cgooMwC5rh5ctV9+aznWmke/P62tr9tlnAPyCOdpwVpv4BCLxzv7qOyVgx0+D0tu5vZib15ntBbm7aulFz7myd1PAAzGrwN6STT/aoRTwIGYuB5SkWywLkxd+/Hh2N
knveX8MYb7c5WoCLTxiJPeDl5ZWvW1dX10kgIigoKCbW9maIOgXSX5OigmUKyfGVGDo/P29oO+XIB0XS0c7ej4+Hd+Ej1r5Agx9awDvtwB/fuwg1BRX/3Sa7b8lZopwHUlpaKj05SlgLgfsgKRYC3lIiRk5PTvgkCUHsDeSvJG/X5kyWdgjpvtb6ai9Fg5nw3tnJ1lbCiXv1q4vSWrlSgHmKjcxYfP+q3zmcBpTHjGX1LyuAxsBujBK9IFjwrYnS
LO+9JjAofVbQaG+QqmChodZOsaseCflih6NuxddxDMptZ6377+ZbDG7WcGHFB2ZZX79iYwWffRZnJsT+PTCQ2CxsdratwczHy1ujEG9jYwM+gRjD1Pd0Im4ziPa+F04nzZiw0bTEAI1K5VSh6BRrb93dtN99vPr3LslaVlOFMV7AcVZjOK+H6fas/U70RwWjP6GG8lAeQocga+BL38YrZawoubE/f/grPcvTS9yEbjL2feRbqV9yPQHx4lUlYc7R
9bKb0PPYX2jad09zDQP3SwveHH4T8SPCwUwtZGJkhEazIbgwzXRXxRg9M4uYS1Z6Zn7+xHA2MSeT00zgzcD1SWTxqRkEV/LaiEj2EYdwkkyOBgr+mAK9VJxVL1PcuifMgki3sLm5ORQGG96SYiaqMSxnaM9RiMPAmnX1kpOX1wy2qr3ftx+SFcgmBaXU9aRscL3DDoUPDAxAo2OiolDbE3qt7O3sYnUp1M6ShYv6uFXYSdiaTDC+NzqzkUSw7A8V
jNlpEhHX8yxFtFtJUiIeYeRUWcvG5c/swmoPVukDSUwu3KBlWUGR/atBlWOW1Qr0dHjYmo9HbMcdG2Yc+Mt1jZiJSjxYqdyxxhV0ScK7LXd3dzfdvHrjGC585h34hwYH+fn5lQQPFr3JC7CDUMqmD5x4pJ/11xyQcKxir+yjrXZxq1x+6jTSb9Lilu2RTDmyzG4RGXpP2cDNUNPH+qtUGzMgdTCim7DOWfFbTGIhMBqibh+x/bOrd/wzRk6tHw4W
JadzmQ6McnmeJgUQIl/AuaHReYGSRN3m3NhvHzYZ4/+aXDK8hEvMUm2KCUcjc5lz1cWCaHFC0Has6FDj6C3q4wYwO7aYPkT1rrqOEnygRFhY0Cb3P8JAjWPvxwmincsCMlITmmltkLk+YqukSRSNpEWf1ZYY1GQmYifB5aUW8bTuH+7mDaJFYTIRy+IyqIxDEeDETGKYYtceDsgOz6JpZGs13QfdB5K6MlZ8/CBX1VcYIxrMAhotfRee+xkJvd52
b+7MQLs3V2AgVZJKb54Y4JSRK2Iyh0utdhkH7LlSysfla0PTBrxEqMeLHgYd3N9fEFtolzeCHdfxKto1lr54wt76CeX97SDZMh6oQf4ZakypZvfXKJPLH3gjWMy6GCKlLMbe33WNpvyDYVvnMSGvhiRWkAuLrcS36xu0CaSn2OMwDxCPFoZuWLy8vdWzxMont5u0sEOk7o7F7/LAlc/LQj9sjXd0BOiWyrLYfpMvSBf6UGLMwlCbDRrwZDmCi3XK
GQjm0pKFh76yPQoDV+tJHHM4DtYnqZx7qgJMqxSyjBYoUS2qdVXAq/Ui/M4EhfDCUVabWhz4qDm3TG2AyiJQeSoqKkpEjlGIVCAt9ob+8+fPTSsn7etymw9YUO/s+ciTrPNGNsbHx4HfFIzpimWRyj5qNZVg4VaVwPnNpeUNBpUkjj5laH3JCyRrqVOFkGdcDu4Sm79i0VgCWQioN2JjH2UNAb6x4vhyv284sCe2lyxK7oA2cDngNTYxE15OqLZB
uS6tejw/tsM48WQpJYbklzqV6oC+G6O6a3xWs2j9JDwMNOArXsthTFZivwOKi1mcJwNupuJoFY+OVvuBiSJiaLh6vX65zHZ0dBS/Krr/en+mMRAfMG1HTKGVVYYMHTDz8XFtdXX1XgvdvBENokS1okCIPokLVqDFV21WzAyHiRKd1ZJ4tSK7VPwQqbm5uVU/4i/ArDZ3drSdOXQrSFHqQ78bcvzNYAkrAchgoqiirq58183tAECiurq6oqIiH6Xx
bZlxk5ZkGHM4Ojo6yLH1M7sM1uHKkiiD7b29vcmmXg99NsNP3/CmJJv5CcYqJLTgKslo3u/QnHFqx6M2ZuiWWCtvmGfVe4QC52ZSz6QkJ9f6YmHwb6U3HvC2YXl9eqRc3kMscBz8zZQhVqlAP+mdg4P2l5Fi9xRTh45j4NaKegkDNsfRVML5j3Kt0PqTOLzPl0ITE6GgThEx/V0qitZAfsuIWDrg9sCtmZeLVdpHbMsqKxcanGj44nc4O2xqrWun
0R/gZxt1W+qWElsNL2pqJfavLnveAsTh9khIcBHKoEWvXGewjpR7fv7Cabax2qobFbDP1tkQm/QA40BLdktxoWv/u9jfigAeDA/mPMPm1J8mWkasJ8QtR/SK/4swu7bs/LjO4C5uPR6h88hM5ZanWkXi7cv/71e/bzWl6w7WvZPqsF7+tzwY9PeWR0CAFIJFHC07cRDy6e/9RkBX8WC98YOngX8PX48VqmZkZGAF5/RwffAnjEEA4DQDvpv4ZNHz
Fk4g7rMUQhpQt7jcHcHVcoCGQ6TN8Xv/Bp0yHiBFydtPEGc/melI2TgoOxaRrEsBflbn5oJ9ouvF0aotXziy8TcpNyX+3TJpe0B29JuhzLqZ3RFbN0fHvd8pLwiTlArXdxsWTJ1lHqmsXpJJEi6Gq8bRO3S4u7tzZlcBE9tr3t9baDdL+fCwpZSY0tYuVY0x3nnt67pnEO24MzEHVogU6qCJJRlJRLfSD5KiMTuWsNEg1HEIiBEVFWpyAbThy03v
1wF5pZPLR6FAaLT0lCX4yEWBPNzvbH9/kcdckpMjqfqZp+5ay/mLYd5Iy8mo8sLd7RmphCxT+DLQwhj/a+Ta2hqX76YySYQskeyKwfOQXivetAGcvTrD+23lwXgYkiA6OMy3PM3MKcr0Uuqsv729BbAqcrEST4nIEPX+s39WOruVnppaUloajBWSG6Ec+OoRq5GNl9f9NjOG8lZQyhUmzdlEBneEGBZwGF5/Mhfi9vujDpoy/cQBG+9/028iaDJl
6DQ5Z1shJecFqWlpxxc+jPGeYFaoPVceSRKSSCQoE2JoPHqEERgtPNvi+WS36zmtHtsOp1Hn/SbpSFQ+1ed3zJ9/pvWvVpi177k3Fzg6/LoF/ZcnH1PD7g0gdG9vz5iTFD9UXl5+ZmbGuoshAr7nhS+tN7h+rM8SWuwjZkaJKvg4rOXu9up+D5e10xngO047jnHm5H5JzIirByWN3x/l43vzxiuphVz4tKVea5HBNk8vl5aWdmcaAMkBDmWkT2uh
b6VeI4Ieo5u6gV6fqX/HliTDUxUaGgogrrIyGZ/JuQxVS6K801y/zl7HBdsGZoGFhcX/7q8VNxlzuJDbBuGJcij88PeLISJxCYaI+L+RiQTWOMZSo26Z6/JyciC7RWQtqqbxJ+KS4U0ELlsVoEScrqxYiVAD9Deqd0Bl+gB9rTFKj/lKOYkz2NCPJb6XKAZBQUGRobaOoPFnX03WeihJCDr0ezbgzPsbKGy6w4nl8jRLtkCx8qeBzI/HOjw+jtHO
+TiAsFJd7IJeJNb5tsOBH25mkJPLTC3oitElzAtBf80Ls7CpbbJq7AUdEkUhQDJGHehaO+2z9vtpKLza98p9+uqVv//dwcFB9WtEfmENezMogTjOOIbyxPmjCx/d0rpzBnZQ05bVIH28A8M0VYWqU5ReEodZXDSg2c+VOLS+vbUVYNtvWb0IFVu4RX0SyBzuHI7RKJTEbkCng8kTcbY7EypnctCdyD82PPxlcnvCvg4a/fpja8LSJ+x8BdT5li+Z
/avDxc+GDa4hzwgsDITS21ag+L/Zbrv2ARVtbHyJNYOfaDoQE2nzUvUF8Us+2inwOBmRL7hLeoWGcnZm+ZS269Wvz9lJZn3m3A5/v7Pcb0+aHx//HlDHy2cN8jt01YKPPLCJotOWz+9m2yx5lZc8avS2+u0kZzFKf41XK7UYZ3iWd+Z6UWGhnJzc8qEHlNLvqJdpSRUEuA23H5aeGlqz4ubfPzbP8R4TqqVDNdO/Tdj380ix/dzFoihZXteswD69
nDhSRt03oSXc2NmhV9RS2n8QsceQZoSjSbT0LWbC/kOTMeuQk2Sj52ojiDdzq6519fUkL0hB9M/JeRbceX9T5chItc0AuYdjnwullEs3cfW0g1rR+7bfn9n7lWaUcDB1hsXOxgmY0wZ0U8z6bGoFKaPDwh7mjUyNjQWbNnYtcVeJN87tua6joOQyG5brnqN9n9llFZRl5fweIBUK13R8+J8Fl3GifOP0AZvamV1cfWtNsQ3dxxoZ3n/FHfiNevdT
LdmTzJTyRj7Bl4BgaFt/lCO5fQqvul8zTEBNiBjlmAiwKiwqYsMp5VEXNJHMkSLpaCkJTiyOdf0wLjFxoRRSp+aR9hZYaR+ODQ+Xksv5lussy6rCbyVX92ePq8K7i1kMAwcL0rvCIdzHJfi6hXKswf5sObkP3tMSfk1N1VqkafY6LSFXt5UkHMwS33dtuo9+xpxFjBHkK65NbHoV2spGgienp7J5I94mOIs3Zwvtk9WtE8cxOoFmJTgZ32FDrNqH
ygDJIVh1hu8U/YqphN1V1NR0x5+HW9I/rjYsB0Lv5WW4erEYTott6eioFoUaIQmlhJ06GAh9kSRqV65hx2VisN480NVv2cJkOltqfqhWqjdybgn0lNadxPrbcjLIA50uPh4eZOcF3w3LI82U8GQ0wQGQfbGQxaukdc8oudqvXzSEMgYdG0qn7Q0hi1f1jQkEYufmoGu6+2MDJTr2z0ifFERGRuqX/7FcIwmiBdBoYYewr5vf3icNheccYVew+hXr
lOrK0sVeXnpNBaFhrXuOUFl8RDzTJGIJRxMIseK9OOrn+vjx4/5Cu79piZX9koLwjkWSD1/imGKcz3rGR/fdvxOjo2iDJLnMW25CtFBxv+vGaYdCZhwSe+u/MprX58gYYOqsyoAOSblMfXaqEVabQhl799YC+t9qE0MSgo9fYVDbA4REgiGcrsbKdKKTe+c/G0g/AhpZLVEk/NSHaD4KjqCO7unpASerqqJSZtZOPSgxhFvlGZEqTSRbOXm16+Tv
vdDme6yj8xjATo4GdgWI86ZemwzMhz1RFNX6kFH0wI40pzaWcDpZzdbW1hQVOaJu4ALGnA+MjGMVVtYG0g7gJ//+ddXjs0ql4g5PTW24NjGHxyXRhed2W3ouBj7oWqoaZ1XY39xkUSOVPew3uz06Ofnukbyjax/2XWg08pvQqH0P8hCA5nlgPtZC03umrKG8rW+blF+V/FjKdXF/qyORyKuzfa5/0pro2Uwd3L8d+K9ubu1d9nIxG0Dx5GuxNM8N
1uwsLmYjkIEs/zof0OTbkdv0dHZ2ErPpdmzNXp5uW5B6AVG+vw00QBlIlVgWwVosh1E9lBJ/qie4wwM8X0XrsAKZR4Plixrhpwn7gfX7fYfmxPm17G/rDJ+RwfhMSidwghadoR3HCja9ICeAALmVbT/KNzczg8tONmpfw361UzuEFrLCCHob//R+L0BK0fdUMBMz3+1eiVDLp3+2T234zwa8IFpCDB5RUbxK/NFJPphFDwLxMChgRAV1OKO6mcJv
wtVS9hhX5AmICEiovrx6gRZjfBi9YkQ3GvXjq3+7ecw44Wue1JzYQQr0jz98+HDAVRNFi02J0Cs3xJTNvsA08NubFSzXvb/1PLkfkEAOkYIAGWSmwSnUV7UKB14lQcsNhZ5v5MX0jT6zR3QGP3jHT/G7K0RKMm+EVllISGjHXVj7gU584War91l4LjbvqL2Skb6hqvMRWgqAcRMTE5EhCggWCBkNMxEP3KdmtF9g6rOTfCbEVHmvgxpJM2w/asCV
oQZv551ZpFJIFRMWFhYUtKsztN6UuMOcXPxE2woCNWAjEIpmHRtAziuoNG39a8XbORZTRk5Ftbu3B9MzsRP6eW1vb+8qlKGBAbPg7df8YDXBXqGWo9HocWjp69vK7uvo6GhpiVbyUDjV/jL07/E0w1mq+PMAs0c3/TU7m2NFzc3N1unq0GgAZe3i7jtfHZx2+hLZwPxzzXO1OC91Jbdfr175mFKzleDDLPzP4TQgmU3Oz3Oq5jNvFSSbCrksfyoT
anBaqNdn71ZjAtQy3+L54u+fBn32kpLCrWc3xoHIRgLx6ypoBu1n4OgrtzO1Hj4HnQ+62UogcOM2nwtT0tap40Tis5Q98T5P8gRvS5Kqbdj9PfoLLaOkSZgobe41P+Oig6ekSl/hR+zSZGUooGslGB1xpbQFmAjsqEsffVM00yfp8P2mJjh+bkGCXQ1JVG86CbG+GNf8D5/37Gv87/+fgySxvwZrItokdXCnc6Woi/25iuTE+aMYRJQccmenzGv+
jryN7OvZ5QNKPhPnMlVJwnm/q3dZQ2fD0gQcKtJ4Z0/O0sy2/ta9TeY0CRoWxtV1jdGIprI3LdMkzFeRDEBCT5eei3/4Ri6unxTVbYkVHEjfm5LyNMm6+ccPEl1TEWqBtIEtgldh1lBYCWZqYexRcXFxgXJyH2WpR2ZWSgoL6dtCMsIKjTWqwRr23GhFxnivVtN6FvW4lfvFals+8jCZP5Vmd0N310OrJ/H0ipoJ46F4aNmslJpJgYWbxpwNPheu
L+Wx/7N/S0VVdeD37wD4baXfaSnoc1sGbAw0p5ZDWD02yAutfjdeLl6KX+gv/r6tDIEDVPn0eMy+bsT2ZLL99qXq2Cof7Yu+Ytaw+XnTzHUd8iZzYB4/kS6YgT5iC/Pzt6eV/jdvxK+m49XFHordMDwLz/296CYkKihYPP8VcDrN7UHo6qBTX3a4Wd+Sc/6Y3XnIv/0FLusmkJGvL44BNAbm8WCP2H6vrNwHUa7GzTkuvmcQE4hk4ZgdBG51xKf6
pnZahs7l5vKDc3u8EGMc/QXILI4Lba05OUvIGHHTWhtVuthG0MKM8bHFUnESG7Av/auLgT17foV2ET0x2qwM3gvhcnmknMYeIKeHt564Nhkf+iSoFXVOPFXiIYPwFi6BK5mb23Vv9swcsuINk8mdjaaCCitFqRRoDcswrFoHnHcQyRaL+Gk0+QMBTe5fdW2Syhnem2vuxHf7P5Wc+T8UWh/H7euQ5FqmZWRrQkgMd4qHyZbdICWyR0hMLoPJluWi
KNtkpIsR2bJOYijScI0tW7jZJ8Y0tzQNZZt6zvQXPM/5/ZzXeZ3zXT7vs3w1gVUfOmEvA9t8G0xKYBskOONij6JE5Ua1Q97VxQAzz6IfJFQezwx1c5v+ujoYUC8rIqApOwg8zNxpn+r2Eu3lleNzKTCQDihQ8xavAhSqW2QLlEG+RI1kei+N1b0EcYoB8j+lz/xkORy41q2pGtfmZ0JvmVvj4S941mH34Zm+rH0+fv4h6GzoO+aWEF8SkDpHgFCo
9JK9Is89SjKjNLLRUgLLkUg7tTyqwxiVLZ5y410wqaWlRT7tsmahDV7LlMrnjSjuwTsbyQhdaXvPkUtgyUWRu8o7p5lGwMFt0bVTKHN4Zuwn0rRV7iQjcpwsY84tZHGH7lrzikikfVnupTwR4EvivrRaDs8/YyglwkWZJWMtu3MxX1UEhnGg59LyS2BrlpbpmnF60NtbU25J8Fr53t5ebu6Odgt+FRudTKUJm0olKwI7aXvvCZdJlNF4YBVEcv83
7G/WCg1pmN0b59yEKgzIKpn2wZz8Pb6b+0wJbFydr84VBlAGDXx544zWi7UF92DVLtZSKSsMnn6Wv65ChlmigkWf9tkzZ/h9Zp4Q2xRFxomiaA9yVH1XlydCNPmCiaAQX/PNNiAMSSRSkyaI2j13f71Fe97XZzFbN7a4uMHC3rUg6hkVWPfky0imH5W01C1ywZY0/Nj3Jbn/ugAFtiwqdMcwW9uzQ+ZoEu/Hk4qC+aiqiTJeZ4kgwrcd3Oqa3F+j
6TRDqUbrDZTroYw3k5MugYEnHprmVozS87YOECq7rt6M+Jk4UFtWxqVUykXAeSuRhkhHjwBIL7XJIUBXofSRxslEaSaTWWaYWa4HtYs4KCx5BD4i+ap1lO4cA4mIirrI2Ue3J2heea4JZloKZprkJcaxB2z1wX0WJ6oTgCOKOEo1on87cNcgbA77k/P98K8Gi10//uJKrrbjLbnVsGoDV1NXUvOQTbwP1VWsFvkfObFUKBS6H/9zaOjufBj+q818
iKMMcIP9+IQB9tqo0fe5W6NGVRY508xTs1qlHpwV3m46ezik9jiYQ7Vpwdcj/e0eWgX3IOJhHwvI3AIXFa7R8c+apewm1s27HnoFv1wYXzUma8S64Susc9dQfKpZXPmMDaKydApsuHcz+M5kkwH19a3d3KxJan0rFlHt6/IZRg2sX2Oe8sxGVJdH/2HudUPFsVkkgMPGSPXLWju4okv9f2useefr9BSLUpMSEbhjKhhfHSeWqwI2KHJj06sT+8Dn
YVwVTAeJFAtkbwiCTAtgeHIwYXs5fdR4r876YiMwR9x8tfaPFUoWJePcKH6/6DrhB9ADmRP9fX0AUDxqGW/E+4xc4xfFCA2Yrd509fhPIPgZGPRHeFyIP6kApLC5kwD1lsBj+s2FzgE2LkIxtyVw9J9QxCz+hh5UsDCaUHmnp3QX37CjJA/ZxRnP7kr06kR/nmN8/qwl+/z9J4pGQZKJ99bHKYp2O4fDmZ+fR5aEYTDj/rrzxDNDLOy5vAF4yuCD
4/KQr+caMM4dzePt7e27u7seWgYIxLWW2fAjBtB7aDCTqncjJWd9TN0JIxFzzzzJX3c6NUtUBoiWOSuGkuk5FHLk72DgjYUublmIePyg6mCq4pO9yyZKaT72VRM0lolSRxZte3Mz0sHhQCe2bBNnnPDpH30RmLJ+37UWoJW5378LwxDFADdjY2Pf7sYOuLuoOhkdbJhgCKAx+q8L6PjZ2ctVE9UTDG/26+VwkEBKBIBCzjBDEdBubv3NoQkJCVr4
8xnu9c/9O3q8v8R2sbD7QGlI1fHznJtGltgTxwC2Li8vhxtC2oSEhRexNIkS+xPDpESzRusoufDtL8uOcBWbaGQJOepTppkS2i8SOZ+raoMzhvVHNFcG3bixtcc50l+WfJ/HboJbd6NH1eF2eevcLfrI/IsI/mdlGZM8iO6phoT1lz85Gxo2gA2qmppAegBpN4uCYa1QhosN+Jki3cvhOpDe5lClB1gQ0S4Vmm8Gq+VVO8JtbW07v/0TooUXv9c9
Hjkjzb/Ae4nQGc/Z3WHTD0OhhtnE67HXrgmtt4gmL8Uc62ex6aO9HSdwb8STE2nbexuvtG1s1fI2OtnZixt4tcoJRttCrLi0eOrrDx9U7guUjYunopAlQo8VtnMtGQP+aBlzxXwhP0WjYzlX6t+ZKTl5eR2w42WMBU2uYii+TbZq/O+VntYD7uemyUgGulQi+c3YrxYwWHjhUIMnGYFAPPeliFmlYCwtLIAoywNkBgeKBJV30UxFOsNMCRjb2ah/
4ZUDGharpn2MyGCSe2kIFAbTlrwmNKQ/M8/YWU6HpRmhi4eFGH5+fiDZmUYkVjCA5oXHy1j/VVz8nhQilxKVuPXHq0XtzPL5MDC4YTbsbBSk8lh+rSP83I9t2t+nhaUB4H2srsI+xqvpQtikhtvA/K1SKHF3ehKt+3WGgvR0IcRp5nxYm95RHM7MKbLdwxtSwV+8CnY2iFDYN8xDeAx4bWDgwiXY+RvmuR+4v+AtiU7XlewmFvfXDGisF3pNZPJh
wycVQPi/Nni1M2yrRvu+J2j6NkhP3Wus7mKIWt7pIlt1upzrnvZbvS84mb0G4lE2d5k4dY672UfPWlPxNvg+qgmTCW89XPFWT0Lu785U1XLKdRek2W4MQE1Fc4KxoSH235nT2AILztJQ0RnZrAD6SAmyRFL0kABrrfgmKlyxNUD9FKD8N76nc/qdH4nYdDW/6ehAXlbuTkyRTdWHDi8tKdpNgMUTm3MyWXAZ3toaGbGXDy27zP35M+XKu51lJT68
igkcefR75Pp9vQeKf+adcDhmjLtEsHKaBMC5A2KS9SGFtJ1eoLdm/HJMywf8rZ+gxylrmLhG1barF3I7b+99e2wU160syouId+p2yqYB7iSc1tMzm9jknsuzUtLS09k7+yA2VGScP3++yHatLP1YrloaqmxhYUHZMht47NLVOzwGUWSjB1Zwsh8UknbvsEHhnJdmknp86iLMX7do5Um+h5Zb52c0JiI1YHbPhOx8EKwD8bp7IXb5taD3j/wVoNcs
VPxElJWV0cjhuPTgx39ptLWh6yMTp4LnO/6o1RwLRchZqutD71Fs9GO7Xs68t8uMadrXfCgdBGiOwlTlHmguUJ/W1mocOwAROn96MIiCkksM/paDidOYshaWh6StSFZMM/mTTJrCBySy1KX5T4G+WOyLbVXv8HBHt9ZTdS/FsjLyPGW/+9ytQV69veVakCFu0j4DVZ2gq2pSup6+7vpTDE07UGMqSmLv/b+14f73InI/hWvo0qfq/UN3eEC7YG5v
9gzlnfxfUEsDBBQAAAgIAGV8uFaOy5PpSAEAAEsGAAASAAAAYXVkaW9zZXR0aW5ncy5qc29u7VLBTsMwDL33K6qchwTHcWSAxKEIqUeEkNe6W7Q2rhxnAk39d9IMtrVLuQISPaX289Pz89slqf8UFKK3mJMz5RNTpWtU16nKnMW8IMb0Bqwu1GwPbsAKsgfswn+oLaEGU/RTl7NjtXpbrEGbAbQ76W+pdg3eLvux5KSphKHYWF9+PoCPDAExYj3U
GS05LjBDgShiv68I66UTtJOYE52D0TXYR+jduiu1EOeubYnF01RQW5wY0mXwMx8YeYaS9zb4XtVOl6+2v0ZFRqbgWzQl9XdQ9/2AOkNFVlDOaFmQqfTKsV+D4jZ2g8qIx3tvhf3hjDyEvVYMprxoNRgaaVXkJH6naGAO3VhwvtvqLEhT0lvgT9FXY6mWasp8JHIBwbjoxoXOxKUDw1d7qCKW/v9Aj+G/JtANCpOhBv9OnOfz+c8EOrxeki75AFBL
AwQUAAAICABlfLhWPtHlky0AAAA3AAAAEQAAAHZpZXdzZXR0aW5ncy5qc29uq+ZSAAKlvPySxJLM/DwlK4VqsAhYtCwztdw3PyUVKKpUkJieqgSWquWq5QIAUEsDBBQAAAgIAGV8uFbjNMmcpQAAAFMBAAAWAAAATUVUQS1JTkYvY29udGFpbmVyLnhtbH2QywrCMBBF9/2KkH0T3bnoA1z4AVLXEtPYjiaZkkkf/r11IVSQ7ObCOXDnFvXiLJtM
IEBf8r3YcWa8xhZ8V/JLc8oPvK6yQqOPCrwJVcZYERDjHayhT9pkdh+tzQcV+5KTxmCuFF/WCEfEZYI9Irq5V/q59mA5OwOuil6STtOP7uYVWJLxe4rBd0lJjS0gmRjX90g8CH0Sn8DM/2n5M0EhN/u8AVBLAQIAAxQAAAgIAGV8uFYt16suHCcAAMUCAQAPAAAAAAAAAAAAAADkgQAAAABzY29yZV9zdHlsZS5tc3NQSwECAAMUAAAICABlfLhW
alpDxJYNAABz5QEAFwAAAAAAAAAAAAAA5IFJJwAAQm9vbXdoYWNrZXJzIC0gUmlvLm1zY3hQSwECAAMUAAAICABlfLhWXvAS6DxNAABbTwAAGAAAAAAAAAAAAAAA5IEUNQAAVGh1bWJuYWlscy90aHVtYm5haWwucG5nUEsBAgADFAAACAgAZXy4Vo7Lk+lIAQAASwYAABIAAAAAAAAAAAAAAOSBhoIAAGF1ZGlvc2V0dGluZ3MuanNvblBLAQIA
AxQAAAgIAGV8uFY+0eWTLQAAADcAAAARAAAAAAAAAAAAAADkgf6DAAB2aWV3c2V0dGluZ3MuanNvblBLAQIAAxQAAAgIAGV8uFbjNMmcpQAAAFMBAAAWAAAAAAAAAAAAAADkgVqEAABNRVRBLUlORi9jb250YWluZXIueG1sUEsFBgAAAAAGAAYAiwEAADOFAAAAAA==
</data>
<mime>application/zip</mime>
<resource-attributes>
<file-name>Boomwhackers - Rio.mscz</file-name>
<source-url>en-cache://tokenKey%3D%22AuthToken%3AUser%3A75937676%22+32040eaf-3c97-1f46-f34c-1d4e8608c682+b687cb1ab2d8f3f10b95c3a500841c66+https://www.evernote.com/shard/s438/res/9521d2af-a8de-26f3-4f86-8f7f4deebb6b</source-url>
</resource-attributes>
</resource>
</note>
</en-export>

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,10 @@
// ====================== IMPORTANT ============================================
// As of 2023-10-23 we should not use these tests anymore as they are too flaky.
// To test the reducer we can use `reducer.test.js` or `app.reducer.test.ts`. If
// it becomes too much of a burden to maintain these `feature_*` tests we may to
// remove them.
// ====================== IMPORTANT ============================================
const { id, ids, createNTestFolders, sortedIds, createNTestNotes, TestApp } = require('@joplin/lib/testing/test-utils.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const uuid = require('@joplin/lib/uuid').default;

View File

@@ -1,4 +1,12 @@
/* eslint-disable no-unused-vars */
// ====================== IMPORTANT ============================================
// As of 2023-10-23 we should not use these tests anymore as they are too flaky.
// To test the reducer we can use `reducer.test.js` or `app.reducer.test.ts`. If
// it becomes too much of a burden to maintain these `feature_*` tests we may to
// remove them.
// ====================== IMPORTANT ============================================
const { setupDatabaseAndSynchronizer, switchClient, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('@joplin/lib/testing/test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder').default;

View File

@@ -1,4 +1,12 @@
/* eslint-disable no-unused-vars */
// ====================== IMPORTANT ============================================
// As of 2023-10-23 we should not use these tests anymore as they are too flaky.
// To test the reducer we can use `reducer.test.js` or `app.reducer.test.ts`. If
// it becomes too much of a burden to maintain these `feature_*` tests we may to
// remove them.
// ====================== IMPORTANT ============================================
const { setupDatabaseAndSynchronizer, switchClient, id, ids, sortedIds, at, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('@joplin/lib/testing/test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder').default;
@@ -7,23 +15,26 @@ const Tag = require('@joplin/lib/models/Tag').default;
const time = require('@joplin/lib/time').default;
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js');
//
// The integration tests are to test the integration of the core system, comprising the
// base application with middleware, reducer and models in response to dispatched events.
// The integration tests are to test the integration of the core system,
// comprising the base application with middleware, reducer and models in
// response to dispatched events.
//
// The general strategy for each integration test is:
// - create a starting application state,
// - inject the event to be tested
// - check the resulting application state
//
// Important: TestApp.wait() must be used after TestApp dispatch to allow the async
// processing to complete
//
// Important: TestApp.wait() must be used after TestApp dispatch to allow the
// async processing to complete
let testApp = null;
describe('integration_ShowAllNotes', () => {
// The below tests can randomly fail, probably due to timing issues, so
// repeat them.
jest.retryTimes(2);
beforeEach(async () => {
testApp = new TestApp();
await testApp.start(['--no-welcome']);

View File

@@ -1,4 +1,12 @@
/* eslint-disable no-unused-vars */
// ====================== IMPORTANT ============================================
// As of 2023-10-23 we should not use these tests anymore as they are too flaky.
// To test the reducer we can use `reducer.test.js` or `app.reducer.test.ts`. If
// it becomes too much of a burden to maintain these `feature_*` tests we may to
// remove them.
// ====================== IMPORTANT ============================================
const { setupDatabaseAndSynchronizer, switchClient, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('@joplin/lib/testing/test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder').default;

View File

@@ -0,0 +1,7 @@
<p><a href=":/62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">Resource link</a></p>
<p><a href="https://example.com/ok" class="jop-noMdConv">ok</a></p>
<p><a href="http://example.com/ok" class="jop-noMdConv">ok</a></p>
<p><a href="mailto:name@email.com" class="jop-noMdConv">ok</a></p>
<p><a href="joplin://62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">ok</a></p>
<p><a href="#" class="jop-noMdConv">not ok</a></p>
<p><a href="#" class="jop-noMdConv">not ok</a></p>

View File

@@ -0,0 +1,13 @@
<a href=":/62d16d1c1e28418da6624fa8742a7ed0">Resource link</a>
<a href="https://example.com/ok">ok</a>
<a href="http://example.com/ok">ok</a>
<a href="mailto:name@email.com">ok</a>
<a href="joplin://62d16d1c1e28418da6624fa8742a7ed0">ok</a>
<a href="file:///etc/passwd">not ok</a>
<a href="data://blabla">not ok</a>

View File

@@ -4,7 +4,7 @@ This is the official Joplin Plugin Repository
## Installation
To install any of these plugins, open the desktop application, then go to the "Plugins" section in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/config_screen.md). You can then search for any plugin and install it from there.
To install any of these plugins, open the desktop application, then go to the "Plugins" section in the [Configuration screen](https://github.com/laurent22/joplin/blob/dev/readme/apps/config_screen.md). You can then search for any plugin and install it from there.
## Plugins

View File

@@ -2,5 +2,20 @@ joplin.plugins.register({
onStart: async function() {
const folder = await joplin.data.post(['folders'], null, { title: "my plugin folder" });
await joplin.data.post(['notes'], null, { parent_id: folder.id, title: "testing plugin!" });
// await joplin.commands.register({
// name: 'updateCurrentNote',
// label: 'Update current note via the data API',
// iconName: 'fas fa-music',
// execute: async () => {
// const noteIds = await joplin.workspace.selectedNoteIds();
// const noteId = noteIds.length === 1 ? noteIds[0] : null;
// if (!noteId) return;
// console.info('Modifying current note...');
// await joplin.data.put(['notes', noteId], null, { body: "New note body " + Date.now() });
// },
// });
// await joplin.views.toolbarButtons.create('updateCurrentNoteButton', 'updateCurrentNote', 'editorToolbar');
},
});

View File

@@ -0,0 +1 @@
just testing

View File

@@ -0,0 +1 @@
just testing 2

View File

@@ -0,0 +1,7 @@
---
title: Example
---
note body

View File

@@ -0,0 +1,4 @@
---
title: Example
---
note body

View File

@@ -1,6 +1,5 @@
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
/* eslint-env es6:false */
/*
* Copyright (c) 2010 Arc90 Inc
*
@@ -55,7 +54,7 @@ var REGEXPS = {
var defaultOptions = { minScore: 20, minContentLength: 140, visibilityChecker: isNodeVisible };
options = Object.assign(defaultOptions, options);
var nodes = doc.querySelectorAll("p, pre");
var nodes = doc.querySelectorAll("p, pre, article");
// Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
// Some articles' DOM structures might look like
@@ -106,5 +105,6 @@ var REGEXPS = {
}
if (typeof module === "object") {
/* global module */
module.exports = isProbablyReaderable;
}

View File

@@ -1,6 +1,5 @@
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
/*eslint-env es6:false*/
/*
* Copyright (c) 2010 Arc90 Inc
*
@@ -27,7 +26,7 @@
* @param {HTMLDocument} doc The document to parse.
* @param {Object} options The options object.
*/
function Readability(doc, options) {
function Readability(doc, options) {
// In some older versions, people passed a URI as the first argument. Cope:
if (options && options.documentElement) {
doc = options;
@@ -56,6 +55,7 @@
return el.innerHTML;
};
this._disableJSONLD = !!options.disableJSONLD;
this._allowedVideoRegex = options.allowedVideoRegex || this.REGEXPS.videos;
// Start with all flags set
this._flags = this.FLAG_STRIP_UNLIKELYS |
@@ -75,12 +75,7 @@
return `<${node.localName} ${attrPairs}>`;
};
this.log = function () {
if (typeof dump !== "undefined") {
var msg = Array.prototype.map.call(arguments, function(x) {
return (x && x.nodeName) ? logNode(x) : x;
}).join(" ");
dump("Reader: (Readability) " + msg + "\n");
} else if (typeof console !== "undefined") {
if (typeof console !== "undefined") {
let args = Array.from(arguments, arg => {
if (arg && arg.nodeType == this.ELEMENT_NODE) {
return logNode(arg);
@@ -89,6 +84,12 @@
});
args.unshift("Reader: (Readability)");
console.log.apply(console, args);
} else if (typeof dump !== "undefined") {
/* global dump */
var msg = Array.prototype.map.call(arguments, function(x) {
return (x && x.nodeName) ? logNode(x) : x;
}).join(" ");
dump("Reader: (Readability) " + msg + "\n");
}
};
} else {
@@ -142,6 +143,9 @@
hashUrl: /^#.+/,
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
// Commas as used in Latin, Sindhi, Chinese and various other scripts.
// see: https://en.wikipedia.org/wiki/Comma#Comma_variants
commas: /\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,
// See: https://schema.org/Article
jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/
},
@@ -898,6 +902,11 @@
let shouldRemoveTitleHeader = true;
while (node) {
if (node.tagName === "HTML") {
this._articleLang = node.getAttribute("lang");
}
var matchString = node.className + " " + node.id;
if (!this._isProbablyVisible(node)) {
@@ -906,6 +915,12 @@
continue;
}
// User is not able to see elements applied with both "aria-modal = true" and "role = dialog"
if (node.getAttribute("aria-modal") == "true" && node.getAttribute("role") == "dialog") {
node = this._removeAndGetNext(node);
continue;
}
// Check to see if this node is a byline, and remove it if it is.
if (this._checkByline(node, matchString)) {
node = this._removeAndGetNext(node);
@@ -1020,7 +1035,7 @@
contentScore += 1;
// Add points for any commas within this paragraph.
contentScore += innerText.split(",").length;
contentScore += innerText.split(this.REGEXPS.commas).length;
// For every 100 characters in this paragraph, add another point. Up to 3 points.
contentScore += Math.min(Math.floor(innerText.length / 100), 3);
@@ -1359,72 +1374,88 @@
_getJSONLD: function (doc) {
var scripts = this._getAllNodesWithTag(doc, ["script"]);
var jsonLdElement = this._findNode(scripts, function(el) {
return el.getAttribute("type") === "application/ld+json";
});
var metadata;
if (jsonLdElement) {
try {
// Strip CDATA markers if present
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
var parsed = JSON.parse(content);
var metadata = {};
if (
!parsed["@context"] ||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
) {
return metadata;
}
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
parsed = parsed["@graph"].find(function(it) {
return (it["@type"] || "").match(
this.REGEXPS.jsonLdArticleTypes
);
});
}
if (
!parsed ||
!parsed["@type"] ||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
) {
return metadata;
}
if (typeof parsed.name === "string") {
metadata.title = parsed.name.trim();
} else if (typeof parsed.headline === "string") {
metadata.title = parsed.headline.trim();
}
if (parsed.author) {
if (typeof parsed.author.name === "string") {
metadata.byline = parsed.author.name.trim();
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
metadata.byline = parsed.author
.filter(function(author) {
return author && typeof author.name === "string";
})
.map(function(author) {
return author.name.trim();
})
.join(", ");
this._forEachNode(scripts, function(jsonLdElement) {
if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") {
try {
// Strip CDATA markers if present
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
var parsed = JSON.parse(content);
if (
!parsed["@context"] ||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
) {
return;
}
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
parsed = parsed["@graph"].find(function(it) {
return (it["@type"] || "").match(
this.REGEXPS.jsonLdArticleTypes
);
});
}
if (
!parsed ||
!parsed["@type"] ||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
) {
return;
}
metadata = {};
if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) {
// we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz
// put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either
// "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default.
var title = this._getArticleTitle();
var nameMatches = this._textSimilarity(parsed.name, title) > 0.75;
var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75;
if (headlineMatches && !nameMatches) {
metadata.title = parsed.headline;
} else {
metadata.title = parsed.name;
}
} else if (typeof parsed.name === "string") {
metadata.title = parsed.name.trim();
} else if (typeof parsed.headline === "string") {
metadata.title = parsed.headline.trim();
}
if (parsed.author) {
if (typeof parsed.author.name === "string") {
metadata.byline = parsed.author.name.trim();
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
metadata.byline = parsed.author
.filter(function(author) {
return author && typeof author.name === "string";
})
.map(function(author) {
return author.name.trim();
})
.join(", ");
}
}
if (typeof parsed.description === "string") {
metadata.excerpt = parsed.description.trim();
}
if (
parsed.publisher &&
typeof parsed.publisher.name === "string"
) {
metadata.siteName = parsed.publisher.name.trim();
}
return;
} catch (err) {
this.log(err.message);
}
if (typeof parsed.description === "string") {
metadata.excerpt = parsed.description.trim();
}
if (
parsed.publisher &&
typeof parsed.publisher.name === "string"
) {
metadata.siteName = parsed.publisher.name.trim();
}
return metadata;
} catch (err) {
this.log(err.message);
}
}
return {};
});
return metadata ? metadata : {};
},
/**
@@ -1623,12 +1654,7 @@
* @param Element
**/
_removeScripts: function(doc) {
this._removeNodes(this._getAllNodesWithTag(doc, ["script"]), function(scriptNode) {
scriptNode.nodeValue = "";
scriptNode.removeAttribute("src");
return true;
});
this._removeNodes(this._getAllNodesWithTag(doc, ["noscript"]));
this._removeNodes(this._getAllNodesWithTag(doc, ["script", "noscript"]));
},
/**
@@ -1818,13 +1844,13 @@
if (isEmbed) {
// First, check the elements attributes to see if any of them contain youtube or vimeo
for (var i = 0; i < element.attributes.length; i++) {
if (this.REGEXPS.videos.test(element.attributes[i].value)) {
if (this._allowedVideoRegex.test(element.attributes[i].value)) {
return false;
}
}
// For embed with <object> tag, check inner HTML as well.
if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) {
if (element.tagName === "object" && this._allowedVideoRegex.test(element.innerHTML)) {
return false;
}
}
@@ -2093,13 +2119,13 @@
for (var i = 0; i < embeds.length; i++) {
// If this embed has attribute that matches video regex, don't delete it.
for (var j = 0; j < embeds[i].attributes.length; j++) {
if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
if (this._allowedVideoRegex.test(embeds[i].attributes[j].value)) {
return false;
}
}
// For embed with <object> tag, check inner HTML as well.
if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) {
if (embeds[i].tagName === "object" && this._allowedVideoRegex.test(embeds[i].innerHTML)) {
return false;
}
@@ -2117,6 +2143,21 @@
(!isList && weight < 25 && linkDensity > 0.2) ||
(weight >= 25 && linkDensity > 0.5) ||
((embedCount === 1 && contentLength < 75) || embedCount > 1);
// Allow simple lists of images to remain in pages
if (isList && haveToRemove) {
for (var x = 0; x < node.children.length; x++) {
let child = node.children[x];
// Don't filter in lists with li's that contain more than one child
if (child.children.length > 1) {
return haveToRemove;
}
}
let li_count = node.getElementsByTagName("li").length;
// Only allow the list to remain if every li contains an image
if (img == li_count) {
return false;
}
}
return haveToRemove;
}
return false;
@@ -2249,6 +2290,7 @@
title: this._articleTitle,
byline: metadata.byline || this._articleByline,
dir: this._articleDir,
lang: this._articleLang,
content: this._serializer(articleContent),
textContent: textContent,
length: textContent.length,
@@ -2259,5 +2301,6 @@
};
if (typeof module === "object") {
/* global module */
module.exports = Readability;
}

View File

@@ -0,0 +1,114 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.getStyleSheets = exports.getImageSizes = void 0;
function absoluteUrl(url) {
if (!url) { return url; }
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) { return url; }
if (url.indexOf('//') === 0) {
return location.protocol + url;
} else if (url[0] === '/') {
return `${location.protocol}//${location.host}${url}`;
} else {
return `${baseUrl()}/${url}`;
}
}
function pageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
// in this case.
if (location.protocol === 'file:') {
return 'file://';
} else {
return location.origin;
}
}
function baseUrl() {
let output = pageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
const output2 = output.split('/');
output2.pop();
output = output2.join('/');
}
return output;
}
function getJoplinClipperSvgClassName(svg) {
for (const className of svg.classList) {
if (className.indexOf('joplin-clipper-svg-') === 0) { return className; }
}
return '';
}
function getImageSizes(element, forceAbsoluteUrls = false) {
const output = {};
const images = element.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.classList && img.classList.contains('joplin-clipper-hidden')) { continue; }
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) { output[src] = []; }
output[src].push({
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
});
}
const svgs = element.getElementsByTagName('svg');
for (let i = 0; i < svgs.length; i++) {
const svg = svgs[i];
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) { continue; }
const className = getJoplinClipperSvgClassName(svg); // 'joplin-clipper-svg-' + i;
if (!className) {
console.warn('SVG without a Joplin class:', svg);
continue;
}
if (!svg.classList.contains(className)) {
svg.classList.add(className);
}
const rect = svg.getBoundingClientRect();
if (!output[className]) { output[className] = []; }
output[className].push({
width: rect.width,
height: rect.height,
});
}
return output;
}
exports.getImageSizes = getImageSizes;
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image) {
if (image.currentSrc) { return image.currentSrc; }
return image.src;
}
// Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an
// option to clip pages as HTML.
// eslint-disable-next-line
function getStyleSheets(doc) {
const output = [];
for (let i = 0; i < doc.styleSheets.length; i++) {
const sheet = doc.styleSheets[i];
try {
for (const cssRule of sheet.cssRules) {
output.push({ type: 'text', value: cssRule.cssText });
}
} catch (error) {
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
// eslint-disable-next-line
console.info('Could not retrieve stylesheet now:', sheet.href);
// eslint-disable-next-line
console.info('It will downloaded by the main application.');
// eslint-disable-next-line
console.info(error);
output.push({ type: 'url', value: sheet.href });
}
}
return output;
}
exports.getStyleSheets = getStyleSheets;
// # sourceMappingURL=clipperUtils.js.map

View File

@@ -20,20 +20,6 @@
browserSupportsPromises_ = false;
}
function absoluteUrl(url) {
if (!url) return url;
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
if (url.indexOf('//') === 0) {
return location.protocol + url;
} else if (url[0] === '/') {
return `${location.protocol}//${location.host}${url}`;
} else {
return `${baseUrl()}/${url}`;
}
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
@@ -49,85 +35,6 @@
return document.title.trim();
}
function pageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
// in this case.
if (location.protocol === 'file:') {
return 'file://';
} else {
return location.origin;
}
}
function baseUrl() {
let output = pageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
output = output.split('/');
output.pop();
output = output.join('/');
}
return output;
}
function getJoplinClipperSvgClassName(svg) {
for (const className of svg.classList) {
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
}
return '';
}
function getImageSizes(element, forceAbsoluteUrls = false) {
const output = {};
const images = element.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) output[src] = [];
output[src].push({
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
});
}
const svgs = element.getElementsByTagName('svg');
for (let i = 0; i < svgs.length; i++) {
const svg = svgs[i];
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
if (!className) {
console.warn('SVG without a Joplin class:', svg);
continue;
}
if (!svg.classList.contains(className)) {
svg.classList.add(className);
}
const rect = svg.getBoundingClientRect();
if (!output[className]) output[className] = [];
output[className].push({
width: rect.width,
height: rect.height,
});
}
return output;
}
function getAnchorNames(element) {
const output = [];
// Anchor names are normally in A tags but can be in SPAN too
@@ -146,14 +53,6 @@
return output;
}
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image) {
if (image.currentSrc) return image.currentSrc;
return image.src;
}
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
// And hard-code the image dimensions so that the information can be used by the clipper server to
// display them at the right sizes in the notes.
@@ -181,6 +80,7 @@
}
if (nodeName === 'img') {
// eslint-disable-next-line no-undef
const src = absoluteUrl(imageSrc(node));
node.setAttribute('src', src);
if (!(src in imageIndexes)) imageIndexes[src] = 0;
@@ -199,6 +99,7 @@
}
if (nodeName === 'svg') {
// eslint-disable-next-line no-undef
const className = getJoplinClipperSvgClassName(node);
if (!(className in imageIndexes)) imageIndexes[className] = 0;
@@ -216,11 +117,13 @@
}
if (nodeName === 'embed') {
// eslint-disable-next-line no-undef
const src = absoluteUrl(node.src);
node.setAttribute('src', src);
}
if (nodeName === 'object') {
// eslint-disable-next-line no-undef
const data = absoluteUrl(node.data);
node.setAttribute('data', data);
}
@@ -300,6 +203,7 @@
let svgId = 0;
for (const svg of svgs) {
// eslint-disable-next-line no-undef
if (!getJoplinClipperSvgClassName(svg)) {
svg.classList.add(`joplin-clipper-svg-${svgId}`);
svgId++;
@@ -307,30 +211,6 @@
}
}
// Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an
// option to clip pages as HTML.
function getStyleSheets(doc) {
const output = [];
for (let i = 0; i < doc.styleSheets.length; i++) {
const sheet = doc.styleSheets[i];
try {
for (const cssRule of sheet.cssRules) {
output.push({ type: 'text', value: cssRule.cssText });
}
} catch (error) {
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
console.info('Could not retrieve stylesheet now:', sheet.href);
console.info('It will downloaded by the main application.');
console.info(error);
output.push({ type: 'url', value: sheet.href });
}
}
return output;
}
function documentForReadability() {
// Readability directly change the passed document so clone it so as
// to preserve the original web page.
@@ -372,7 +252,9 @@
name: shouldSendToJoplin ? 'sendContentToJoplin' : 'clippedContent',
title: title,
html: html,
// eslint-disable-next-line no-undef
base_url: baseUrl(),
// eslint-disable-next-line no-undef
url: pageLocationOrigin() + location.pathname + location.search,
parent_id: command.parent_id,
tags: command.tags || '',
@@ -397,6 +279,7 @@
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
return response;
}
// eslint-disable-next-line no-undef
return clippedContentResponse(article.title, article.body, getImageSizes(document), getAnchorNames(document));
} else if (command.name === 'isProbablyReaderable') {
@@ -408,6 +291,7 @@
} else if (command.name === 'completePageHtml') {
if (isPagePdf()) {
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
}
@@ -417,11 +301,34 @@
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
// directly on the document, so we make a copy of it first.
const cleanDocument = document.body.cloneNode(true);
// eslint-disable-next-line no-undef
const imageSizes = getImageSizes(document, true);
const imageIndexes = {};
cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
// eslint-disable-next-line no-undef
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
// The <BODY> tag may have a style in the CSS stylesheets. This
// style can be overriden by setting the `style` attribute on the
// BODY tag. Since we don't keep the body tag, it means we may be
// missing some styling, which may break the page.
//
// For example, on this page:
// https://devblogs.microsoft.com/oldnewthing/20180529-00/?p=98855
// The BODY tag has visibility set to hidden in the stylesheet, and
// made visible by setting the style attribute. Because of that,
// previously that imported note would show blank content, while now
// it will be visible.
//
// Fixes https://github.com/laurent22/joplin/issues/7925
if (document.body.getAttribute('style')) {
stylesheets.push({
type: 'text',
value: `body { ${document.body.getAttribute('style')} }`,
});
}
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes, getAnchorNames(document), stylesheets);
} else if (command.name === 'selectedHtml') {
@@ -441,9 +348,11 @@
container.appendChild(range.cloneContents());
}
// eslint-disable-next-line no-undef
const imageSizes = getImageSizes(document, true);
const imageIndexes = {};
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
} else if (command.name === 'screenshot') {
@@ -546,6 +455,7 @@
const content = {
title: pageTitle(),
crop_rect: selectionArea,
// eslint-disable-next-line no-undef
url: pageLocationOrigin() + location.pathname + location.search,
parent_id: command.parent_id,
tags: command.tags,
@@ -570,7 +480,9 @@
} else if (command.name === 'pageUrl') {
// eslint-disable-next-line no-undef
const url = pageLocationOrigin() + location.pathname + location.search;
// eslint-disable-next-line no-undef
return clippedContentResponse(pageTitle(), url, getImageSizes(document), getAnchorNames(document));
} else {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.13.0",
"version": "2.13.1",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -1,6 +1,8 @@
const fs = require('fs-extra');
const sourcePath = `${__dirname}/../../lib/randomClipperPort.js`;
const clipperUtilsPath = `${__dirname}/../../lib/clipperUtils.js`;
// Mozilla insists on building the clipper from a tarball, not from the repository
// so we add this check and only copy the file if it's present. Normally it rarely
@@ -10,6 +12,10 @@ if (fs.pathExistsSync(sourcePath)) {
fs.copySync(sourcePath, `${__dirname}/src/randomClipperPort.js`);
}
if (fs.pathExistsSync(clipperUtilsPath)) {
fs.copySync(clipperUtilsPath, `${__dirname}/../content_scripts/clipperUtils.js`);
}
// These files give warnings when loading the extension in Chrome, in dev mode
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_key.pem`);
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_rsa_pubkey.pem`);

View File

@@ -132,7 +132,7 @@ class AppComponent extends Component {
};
this.clipperServerHelpLink_click = () => {
bridge().tabsCreate({ url: 'https://joplinapp.org/clipper/' });
bridge().tabsCreate({ url: 'https://joplinapp.org/help/apps/clipper' });
};
this.folderSelect_change = (event) => {
@@ -182,6 +182,7 @@ class AppComponent extends Component {
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
}

View File

@@ -14,3 +14,7 @@ style.min.css
build/lib/
vendor/*
!vendor/loadEmojiLib.js
test-results/
playwright-report/
playwright/.cache/
integration-tests/test-profile/

View File

@@ -4,11 +4,15 @@ import shim from '@joplin/lib/shim';
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, Tray, screen } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
const { dirname } = require('@joplin/lib/path-utils');
const fs = require('fs-extra');
const { ipcMain } = require('electron');
import { dialog, ipcMain } from 'electron';
import { _ } from '@joplin/lib/locale';
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
interface RendererProcessQuitReply {
canClose: boolean;
@@ -33,7 +37,7 @@ export default class ElectronAppWrapper {
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
public constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
@@ -65,6 +69,42 @@ export default class ElectronAppWrapper {
return this.initialCallbackUrl_;
}
// Call when the app fails in a significant way.
//
// Assumes that the renderer process may be in an invalid state and so cannot
// be accessed.
public async handleAppFailure(errorMessage: string, canIgnore: boolean, isTesting?: boolean) {
const buttons = [];
buttons.push(_('Quit'));
const exitIndex = 0;
if (canIgnore) {
buttons.push(_('Ignore'));
}
const restartIndex = buttons.length;
buttons.push(_('Restart in safe mode'));
const { response } = await dialog.showMessageBox({
message: _('An error occurred: %s', errorMessage),
buttons,
});
if (response === restartIndex) {
await restartInSafeModeFromMain();
// A hung renderer seems to prevent the process from exiting completely.
// In this case, crashing the renderer allows the window to close.
//
// Also only run this if not testing (crashing the renderer breaks automated
// tests).
if (this.win_ && !this.win_.webContents.isCrashed() && !isTesting) {
this.win_.webContents.forcefullyCrashRenderer();
}
} else if (response === exitIndex) {
process.exit(1);
}
}
public createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
@@ -120,6 +160,20 @@ export default class ElectronAppWrapper {
this.win_.setPosition(primaryDisplayWidth / 2 - windowWidth, primaryDisplayHeight / 2 - windowHeight);
}
this.win_.webContents.on('unresponsive', async () => {
await this.handleAppFailure(_('Window unresponsive.'), true);
});
this.win_.webContents.on('render-process-gone', async _event => {
await this.handleAppFailure('Renderer process gone.', false);
});
this.win_.webContents.on('did-fail-load', async event => {
if ((event as any).isMainFrame) {
await this.handleAppFailure('Renderer process failed to load', false);
}
});
void this.win_.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
@@ -142,6 +196,15 @@ export default class ElectronAppWrapper {
}, 3000);
}
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
this.win_.webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
this.win_.on('close', (event: any) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the

View File

@@ -67,6 +67,7 @@ import eventManager from '@joplin/lib/eventManager';
import path = require('path');
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseNotesParent } from '@joplin/lib/reducer';
const pluginClasses = [
require('./plugins/GotoAnything').default,
@@ -192,7 +193,7 @@ class Application extends BaseApplication {
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
const css = `.CodeMirror *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.appendChild(document.createTextNode(css));
@@ -447,10 +448,35 @@ class Application extends BaseApplication {
// items: masterKeys,
// });
this.store().dispatch({
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
const getNotesParent = async () => {
let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
notesParent = {
type: 'Folder',
selectedItemId: Setting.value('activeFolderId'),
};
}
return notesParent;
};
const notesParent = await getNotesParent();
if (notesParent.type === 'SmartFilter') {
this.store().dispatch({
type: 'SMART_FILTER_SELECT',
id: notesParent.selectedItemId,
});
} else if (notesParent.type === 'Tag') {
this.store().dispatch({
type: 'TAG_SELECT',
id: notesParent.selectedItemId,
});
} else {
this.store().dispatch({
type: 'FOLDER_SELECT',
id: notesParent.selectedItemId,
});
}
this.store().dispatch({
type: 'FOLDER_SET_COLLAPSED_ALL',

View File

@@ -184,11 +184,16 @@ export class Bridge {
return dialog.showMessageBoxSync(window, options);
}
public showErrorMessageBox(message: string) {
public showErrorMessageBox(message: string, options: any = null) {
options = {
buttons: [_('OK')],
...options,
};
return this.showMessageBox_(this.window(), {
type: 'error',
message: message,
buttons: [_('OK')],
buttons: options.buttons,
});
}

View File

@@ -6,7 +6,7 @@ import KvStore from '@joplin/lib/services/KvStore';
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
const packageInfo = require('./packageInfo.js');
const compareVersions = require('compare-versions');
import { compareVersions } from 'compare-versions';
const logger = Logger.create('checkForUpdates');
@@ -113,7 +113,7 @@ export default async function checkForUpdates(inBackground: boolean, parentWindo
} else if (buttonIndex === 1) {
await addSkippedVersion(release.version);
} else if (buttonIndex === 2) {
void bridge().openExternal('https://joplinapp.org/changelog/');
void bridge().openExternal('https://joplinapp.org/help/about/changelog/desktop');
}
}
}

View File

@@ -10,7 +10,7 @@ interface Props {
}
const openMissingPasswordFAQ = () =>
bridge().openExternal('https://joplinapp.org/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin');
bridge().openExternal('https://joplinapp.org/help/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin');
// A link to a specific part of the FAQ related to passwords being cleared when upgrading
// to a MacOS/ARM release.

View File

@@ -260,7 +260,7 @@ const EncryptionConfigScreen = (props: Props) => {
<br/>
<MacOSMissingPasswordHelpLink
theme={theme}
text={_('%s: Missing password', _('Help'))}
text={_('%s: Missing password.', _('Help'))}
/>
</p>
);

View File

@@ -62,6 +62,7 @@ const useKeymap = (): [
// Then, update the state with the data from KeymapService
// Side-effect: Changes will also be saved to the disk
setKeymapItems(keymapService.getKeymapItems());
setMustSave(true);
} catch (error) {
// oldKeymapItems includes even the unchanged keymap items
// However, it is not an issue because the logic accounts for such scenarios

View File

@@ -79,6 +79,7 @@ interface Props {
startupPluginsLoaded: boolean;
shareInvitations: ShareInvitation[];
isSafeMode: boolean;
enableBetaMarkdownEditor: boolean;
needApiAuth: boolean;
processingShareInvitationResponse: boolean;
isResettingLayout: boolean;
@@ -737,7 +738,13 @@ class MainScreenComponent extends React.Component<Props, State> {
},
editor: () => {
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
if (this.props.isSafeMode) {
bodyEditor = 'PlainText';
} else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
bodyEditor = 'CodeMirror6';
}
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
},
};
@@ -909,6 +916,7 @@ const mapStateToProps = (state: AppState) => {
shareInvitations: state.shareService.shareInvitations,
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
isSafeMode: state.settings.isSafeMode,
enableBetaMarkdownEditor: state.settings['editor.beta'],
needApiAuth: state.needApiAuth,
showInstallTemplatesPlugin: state.hasLegacyTemplates && !state.pluginService.plugins['joplin.plugin.templates'],
isResettingLayout: state.isResettingLayout,

View File

@@ -9,6 +9,7 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import versionInfo from '@joplin/lib/versionInfo';
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import InteropServiceHelper from '../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
@@ -316,14 +317,30 @@ function useMenu(props: Props) {
bridge().showErrorMessageBox(error.message);
}
if (errors.length) {
bridge().showErrorMessageBox('There was some errors importing the notes. Please check the console for more details.');
props.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
}
void CommandService.instance().execute('hideModalMessage');
if (errors.length) {
const response = bridge().showErrorMessageBox('There was some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
buttons: [_('Close'), _('Send bug report')],
});
props.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
if (response === 1) {
const url = makeDiscourseDebugUrl(
`Error importing notes from format: ${module.format}`,
`- Input format: ${module.format}\n- Output format: ${module.outputFormat}`,
errors,
packageInfo,
PluginService.instance(),
props.pluginSettings,
);
void bridge().openExternal(url);
}
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.selectedFolderId]);
}, [props.selectedFolderId, props.pluginSettings]);
const onMenuItemClickRef = useRef(null);
onMenuItemClickRef.current = onMenuItemClick;
@@ -817,6 +834,7 @@ function useMenu(props: Props) {
menuItemDic.setTags,
menuItemDic.showShareNoteDialog,
separator(),
menuItemDic.showNoteProperties,
menuItemDic.showNoteContentProperties,
],
},

View File

@@ -1,60 +0,0 @@
import { NoteBodyEditorProps } from '../../../utils/types';
const { buildStyle } = require('@joplin/lib/theme');
export default function styles(props: NoteBodyEditorProps) {
return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: any) => {
return {
root: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
...props.style,
},
rowToolbar: {
position: 'relative',
display: 'flex',
flexDirection: 'row',
},
rowEditorViewer: {
position: 'relative',
display: 'flex',
flexDirection: 'row',
flex: 1,
paddingTop: 10,
},
cellEditor: {
position: 'relative',
display: 'flex',
flex: 1,
},
cellViewer: {
position: 'relative',
display: 'flex',
flex: 1,
borderLeftWidth: 1,
borderLeftColor: theme.dividerColor,
borderLeftStyle: 'solid',
},
viewer: {
display: 'flex',
overflow: 'hidden',
verticalAlign: 'top',
boxSizing: 'border-box',
width: '100%',
},
editor: {
display: 'flex',
width: 'auto',
height: 'auto',
flex: 1,
overflowY: 'hidden',
paddingTop: 0,
lineHeight: `${Math.round(17 * props.fontSize / 12)}px`,
fontSize: `${props.fontSize}px`,
color: theme.color,
backgroundColor: theme.backgroundColor,
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js
},
};
});
}

View File

@@ -0,0 +1,12 @@
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
const setupVim = (CodeMirror: CodeMirrorControl) => {
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
};
export default setupVim;

View File

@@ -0,0 +1,168 @@
import { ContextMenuEvent, ContextMenuParams } from 'electron';
import { useEffect, RefObject } from 'react';
import { _ } from '@joplin/lib/locale';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import eventManager from '@joplin/lib/eventManager';
import bridge from '../../../../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const menuUtils = new MenuUtils(CommandService.instance());
interface ContextMenuProps {
plugins: PluginStates;
editorCutText: ()=> void;
editorCopyText: ()=> void;
editorPaste: ()=> void;
editorRef: RefObject<CodeMirrorControl>;
editorClassName: string;
}
const useContextMenu = (props: ContextMenuProps) => {
const editorRef = props.editorRef;
// The below code adds support for spellchecking when it is enabled
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
for (; elem.parentElement; elem = elem.parentElement) {
if (elem.classList.contains(props.editorClassName)) {
return true;
}
}
return false;
};
const convertFromScreenCoordinates = (zoomPercent: number, screenXY: number) => {
const zoomFraction = zoomPercent / 100;
return screenXY / zoomFraction;
};
function pointerInsideEditor(params: ContextMenuParams) {
const x = params.x, y = params.y, isEditable = params.isEditable;
const elements = document.getElementsByClassName(props.editorClassName);
// Note: We can't check inputFieldType here. When spellcheck is enabled,
// params.inputFieldType is "none". When spellcheck is disabled,
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
if (!elements.length || !isEditable) return false;
// Checks whether the element the pointer clicked on is inside the editor.
// This logic will need to be changed if the editor is eventually wrapped
// in an iframe, as elementFromPoint will return the iframe container (and not
// a child of the editor).
const zoom = Setting.value('windowContentZoomFactor');
const xScreen = convertFromScreenCoordinates(zoom, x);
const yScreen = convertFromScreenCoordinates(zoom, y);
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
}
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
if (!pointerInsideEditor(params)) return;
// Don't show the default menu.
event.preventDefault();
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
props.editorCutText();
},
}),
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
props.editorCopyText();
},
}),
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
props.editorPaste();
},
}),
);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
menu.append(new MenuItem(item));
}
// CodeMirror 5 only:
// Typically CodeMirror handles all interactions itself (highlighting etc.)
// But in the case of clicking a mispelled word, we need electron to handle the click
// The result is that CodeMirror doesn't know what's been selected and doesn't
// move the cursor into the correct location.
// and when the user selects a new spelling it will be inserted in the wrong location
// So in this situation, we use must manually align the internal codemirror selection
// to the contextmenu selection
if (editorRef.current && !editorRef.current.cm6 && spellCheckerMenuItems.length > 0) {
(editorRef.current as any).alignSelection(params);
}
let filterObject: EditContextMenuFilterObject = {
items: [],
};
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
for (const item of filterObject.items) {
menu.append(new MenuItem({
label: item.label,
click: async () => {
const args = item.commandArgs || [];
void CommandService.instance().execute(item.commandName, ...args);
},
type: item.type,
}));
}
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup();
}
// Prepend the event listener so that it gets called before
// the listener that shows the default menu.
bridge().window().webContents.prependListener('context-menu', onContextMenu);
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
};
}, [
props.plugins, props.editorClassName, editorRef,
props.editorCutText, props.editorCopyText, props.editorPaste,
]);
};
export default useContextMenu;

View File

@@ -4,6 +4,7 @@ import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
import { EditorCommand } from '../../../utils/types';
import shim from '@joplin/lib/shim';
import { reg } from '@joplin/lib/registry';
import setupVim from './setupVim';
export default function useKeymap(CodeMirror: any) {
@@ -17,14 +18,6 @@ export default function useKeymap(CodeMirror: any) {
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
}
function setupVim() {
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
}
function isEditorCommand(command: string) {
return command.startsWith('editor.');
}
@@ -184,7 +177,7 @@ export default function useKeymap(CodeMirror: any) {
keymapService.on('keymapChange', registerKeymap);
setupEmacs();
setupVim();
setupVim(CodeMirror);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@@ -0,0 +1,82 @@
import { Theme } from '@joplin/lib/themes/type';
import { NoteBodyEditorProps } from '../../../utils/types';
import { buildStyle } from '@joplin/lib/theme';
import { useMemo } from 'react';
const useStyles = (props: NoteBodyEditorProps) => {
return useMemo(() => {
return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: Theme) => {
return {
root: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
minHeight: 0,
...props.style,
},
rowToolbar: {
position: 'relative',
display: 'flex',
flexDirection: 'row',
},
rowEditorViewer: {
position: 'relative',
display: 'flex',
flexDirection: 'row',
flex: 1,
paddingTop: 10,
// Allow the editor container to shrink (allowing the editor to scroll)
minHeight: 0,
},
cellEditor: {
position: 'relative',
display: 'flex',
flex: 1,
},
cellViewer: {
position: 'relative',
display: 'flex',
flex: 1,
borderLeftWidth: 1,
borderLeftColor: theme.dividerColor,
borderLeftStyle: 'solid',
},
viewer: {
display: 'flex',
overflow: 'hidden',
verticalAlign: 'top',
boxSizing: 'border-box',
width: '100%',
},
editor: {
display: 'flex',
width: 'auto',
height: 'auto',
flex: 1,
overflowY: 'hidden',
paddingTop: 0,
lineHeight: `${Math.round(17 * props.fontSize / 12)}px`,
fontSize: `${props.fontSize}px`,
color: theme.color,
backgroundColor: theme.backgroundColor,
// CM5 only
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js
},
// CM6 only
globalTheme: {
...theme,
fontFamily: 'inherit',
fontSize: props.fontSize,
fontSizeUnits: 'px',
isDesktop: true,
},
};
});
}, [props.style, props.themeId, props.fontSize]);
};
export default useStyles;

View File

@@ -0,0 +1,46 @@
import type CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
import shared from '@joplin/lib/components/shared/note-screen-shared';
import { useCallback, RefObject } from 'react';
interface Props {
onMessage(event: any): void;
getLineScrollPercent(): number;
setEditorPercentScroll(fraction: number): void;
editorRef: RefObject<CodeMirror5Emulation>;
content: string;
}
const useWebviewIpcMessage = (props: Props) => {
const editorRef = props.editorRef;
return useCallback((event: any) => {
const msg = event.channel ? event.channel : '';
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
if (msg.indexOf('checkboxclick:') === 0) {
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
if (editorRef.current) {
// To cancel CodeMirror's layout drift, the scroll position
// is recorded before updated, and then it is restored.
// Ref. https://github.com/laurent22/joplin/issues/5890
const percent = props.getLineScrollPercent();
editorRef.current.replaceRange(line, from, to);
props.setEditorPercentScroll(percent);
}
} else if (msg === 'percentScroll') {
const percent = arg0;
props.setEditorPercentScroll(percent);
} else {
props.onMessage(event);
}
}, [
props.onMessage,
props.content,
editorRef,
props.getLineScrollPercent,
props.setEditorPercentScroll,
]);
};
export default useWebviewIpcMessage;

View File

@@ -1,55 +1,45 @@
import * as React from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
// eslint-disable-next-line no-unused-vars
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
import { CommandValue } from '../../utils/types';
import { usePrevious, cursorPositionToTextOffset } from './utils';
import useScrollHandler from './utils/useScrollHandler';
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
import { CommandValue } from '../../../utils/types';
import { usePrevious, cursorPositionToTextOffset } from '../utils';
import useScrollHandler from '../utils/useScrollHandler';
import useElementSize from '@joplin/lib/hooks/useElementSize';
import Toolbar from './Toolbar';
import styles_ from './styles';
import { RenderedBody, defaultRenderedBody } from './utils/types';
import NoteTextViewer from '../../../NoteTextViewer';
import Toolbar from '../Toolbar';
import { RenderedBody, defaultRenderedBody } from '../utils/types';
import NoteTextViewer from '../../../../NoteTextViewer';
import Editor from './Editor';
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
import usePluginServiceRegistration from '../../../utils/usePluginServiceRegistration';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../../services/bridge';
import bridge from '../../../../../services/bridge';
import markdownUtils from '@joplin/lib/markdownUtils';
import shim from '@joplin/lib/shim';
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
import { ThemeAppearance } from '@joplin/lib/themes/type';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import dialogs from '../../../dialogs';
import convertToScreenCoordinates from '../../../utils/convertToScreenCoordinates';
import dialogs from '../../../../dialogs';
import { MarkupToHtml } from '@joplin/renderer';
const { clipboard } = require('electron');
const debounce = require('debounce');
import shared from '@joplin/lib/components/shared/note-screen-shared';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
import eventManager from '@joplin/lib/eventManager';
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
import type { ContextMenuEvent, ContextMenuParams } from 'electron';
const menuUtils = new MenuUtils(CommandService.instance());
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
import useStyles from '../utils/useStyles';
import useContextMenu from '../utils/useContextMenu';
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return { ...override };
}
function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const styles = styles_(props);
function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) {
const styles = useStyles(props);
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
const [renderedBodyContentKey, setRenderedBodyContentKey] = useState<string>(null);
@@ -146,7 +136,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.insertAtCursor(cmd.value.markdownTags.join('\n'));
} else if (cmd.value.type === 'files') {
const pos = cursorPositionToTextOffset(editorRef.current.getCursor(), props.content);
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL, position: pos });
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, {
createFileURL: !!cmd.value.createFileURL,
position: pos,
markupLanguage: props.contentMarkupLanguage,
});
editorRef.current.updateBody(newBody);
} else {
reg.logger().warn('CodeMirror: unsupported drop item: ', cmd);
@@ -224,7 +218,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const cursor = editorRef.current.getCursor();
const pos = cursorPositionToTextOffset(cursor, props.content);
const newBody = await commandAttachFileToBody(props.content, null, { position: pos });
const newBody = await commandAttachFileToBody(props.content, null, { position: pos, markupLanguage: props.contentMarkupLanguage });
if (newBody) editorRef.current.updateBody(newBody);
},
textNumberedList: () => {
@@ -265,7 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
},
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
}, [props.content, props.visiblePanes, props.contentMarkupLanguage, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
const onEditorPaste = useCallback(async (event: any = null) => {
const resourceMds = await getResourcesFromPasteEvent(event);
@@ -599,29 +593,13 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
setWebviewReady(true);
}, []);
const webview_ipcMessage = useCallback((event: any) => {
const msg = event.channel ? event.channel : '';
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
if (msg.indexOf('checkboxclick:') === 0) {
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
if (editorRef.current) {
// To cancel CodeMirror's layout drift, the scroll position
// is recorded before updated, and then it is restored.
// Ref. https://github.com/laurent22/joplin/issues/5890
const percent = getLineScrollPercent();
editorRef.current.replaceRange(line, from, to);
setEditorPercentScroll(percent);
}
} else if (msg === 'percentScroll') {
const percent = arg0;
setEditorPercentScroll(percent);
} else {
props.onMessage(event);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.onMessage, props.content, setEditorPercentScroll]);
const webview_ipcMessage = useWebviewIpcMessage({
editorRef,
setEditorPercentScroll,
getLineScrollPercent,
content: props.content,
onMessage: props.onMessage,
});
useEffect(() => {
let cancelled = false;
@@ -750,19 +728,29 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return output;
}, [styles.cellViewer, props.visiblePanes]);
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
// Disable this effect to fix this:
//
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
// to automatically focus the editor when the layout changes. The workaround
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
//
// On the other hand, if we automatically focus the editor, and the user
// does not want this, there's no workaround, so it's better to have this
// disabled.
useEffect(() => {
if (!editorRef.current) return;
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
// Anytime the user toggles the visible panes AND the editor is visible as a result
// we should focus the editor
// The intuition is that a panel toggle (with editor in view) is the equivalent of
// an editor interaction so users should expect the editor to be focused
if (editorPaneVisible) {
editorRef.current.focus();
}
}, [editorPaneVisible]);
// useEffect(() => {
// if (!editorRef.current) return;
// // Anytime the user toggles the visible panes AND the editor is visible as a result
// // we should focus the editor
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
// // an editor interaction so users should expect the editor to be focused
// if (editorPaneVisible) {
// editorRef.current.focus();
// }
// }, [editorPaneVisible]);
useEffect(() => {
if (!editorRef.current) return;
@@ -779,142 +767,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.refresh();
}, [rootSize, styles.editor, props.visiblePanes]);
// The below code adds support for spellchecking when it is enabled
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
const isAncestorOfCodeMirrorEditor = (elem: HTMLElement) => {
for (; elem.parentElement; elem = elem.parentElement) {
if (elem.classList.contains('codeMirrorEditor')) {
return true;
}
}
return false;
};
let lastInCodeMirrorContextMenuTimestamp = 0;
// The browser's contextmenu event provides additional information about the
// target of the event, not provided by the Electron context-menu event.
const onBrowserContextMenu = (event: Event) => {
if (isAncestorOfCodeMirrorEditor(event.target as HTMLElement)) {
lastInCodeMirrorContextMenuTimestamp = Date.now();
}
};
function pointerInsideEditor(params: ContextMenuParams) {
const x = params.x, y = params.y, isEditable = params.isEditable;
const elements = document.getElementsByClassName('codeMirrorEditor');
// Note: We can't check inputFieldType here. When spellcheck is enabled,
// params.inputFieldType is "none". When spellcheck is disabled,
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
if (!elements.length || !isEditable) return false;
const maximumMsSinceBrowserEvent = 100;
if (Date.now() - lastInCodeMirrorContextMenuTimestamp > maximumMsSinceBrowserEvent) {
return false;
}
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
}
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
if (!pointerInsideEditor(params)) return;
// Don't show the default menu.
event.preventDefault();
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
}),
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
}),
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
editorPaste();
},
}),
);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
menu.append(new MenuItem(item));
}
// Typically CodeMirror handles all interactions itself (highlighting etc.)
// But in the case of clicking a mispelled word, we need electron to handle the click
// The result is that CodeMirror doesn't know what's been selected and doesn't
// move the cursor into the correct location.
// and when the user selects a new spelling it will be inserted in the wrong location
// So in this situation, we use must manually align the internal codemirror selection
// to the contextmenu selection
if (editorRef.current && spellCheckerMenuItems.length > 0) {
editorRef.current.alignSelection(params);
}
let filterObject: EditContextMenuFilterObject = {
items: [],
};
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
for (const item of filterObject.items) {
menu.append(new MenuItem({
label: item.label,
click: async () => {
const args = item.commandArgs || [];
void CommandService.instance().execute(item.commandName, ...args);
},
type: item.type,
}));
}
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup();
}
// Prepend the event listener so that it gets called before
// the listener that shows the default menu.
bridge().window().webContents.prependListener('context-menu', onContextMenu);
window.addEventListener('contextmenu', onBrowserContextMenu);
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
window.removeEventListener('contextmenu', onBrowserContextMenu);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.plugins]);
useContextMenu({
plugins: props.plugins,
editorCutText, editorCopyText, editorPaste,
editorRef,
editorClassName: 'codeMirrorEditor',
});
function renderEditor() {

View File

@@ -12,15 +12,15 @@ import 'codemirror/addon/scroll/annotatescrollbar';
import 'codemirror/addon/search/matchesonscrollbar';
import 'codemirror/addon/search/searchcursor';
import useListIdent from './utils/useListIdent';
import useScrollUtils from './utils/useScrollUtils';
import useCursorUtils from './utils/useCursorUtils';
import useLineSorting from './utils/useLineSorting';
import useEditorSearch from './utils/useEditorSearch';
import useJoplinMode from './utils/useJoplinMode';
import useKeymap from './utils/useKeymap';
import useExternalPlugins from './utils/useExternalPlugins';
import useJoplinCommands from './utils/useJoplinCommands';
import useListIdent from '../utils/useListIdent';
import useScrollUtils from '../utils/useScrollUtils';
import useCursorUtils from '../utils/useCursorUtils';
import useLineSorting from '../utils/useLineSorting';
import useEditorSearch from '../utils/useEditorSearch';
import useJoplinMode from '../utils/useJoplinMode';
import useKeymap from '../utils/useKeymap';
import useExternalPlugins from '../utils/useExternalPlugins';
import useJoplinCommands from '../utils/useJoplinCommands';
import 'codemirror/keymap/emacs';
import 'codemirror/keymap/vim';

View File

@@ -0,0 +1,440 @@
import * as React from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
import NoteTextViewer from '../../../../NoteTextViewer';
import Editor from './Editor';
import usePluginServiceRegistration from '../../../utils/usePluginServiceRegistration';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../../../services/bridge';
import shim from '@joplin/lib/shim';
import { MarkupToHtml } from '@joplin/renderer';
const { clipboard } = require('electron');
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
import { EditorKeymap, EditorLanguageType, EditorSettings } from '@joplin/editor/types';
import useStyles from '../utils/useStyles';
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
import useScrollHandler from '../utils/useScrollHandler';
import Logger from '@joplin/utils/Logger';
import useEditorCommands from './useEditorCommands';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import useContextMenu from '../utils/useContextMenu';
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import Toolbar from '../Toolbar';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
interface RenderedBody {
html: string;
pluginAssets: any[];
}
function defaultRenderedBody(): RenderedBody {
return {
html: '',
pluginAssets: [],
};
}
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return { ...override };
}
const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
const styles = useStyles(props);
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
const [renderedBodyContentKey, setRenderedBodyContentKey] = useState<string>(null);
const [webviewReady, setWebviewReady] = useState(false);
const editorRef = useRef<CodeMirrorControl>(null);
const rootRef = useRef(null);
const webviewRef = useRef(null);
type OnChangeCallback = (event: OnChangeEvent)=> void;
const props_onChangeRef = useRef<OnChangeCallback>(null);
props_onChangeRef.current = props.onChange;
const [selectionRange, setSelectionRange] = useState({ from: 0, to: 0 });
const {
resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, getLineScrollPercent,
} = useScrollHandler(editorRef, webviewRef, props.onScroll);
usePluginServiceRegistration(ref);
const codeMirror_change = useCallback((newBody: string) => {
if (newBody !== props.content) {
props_onChangeRef.current({ changeId: null, content: newBody });
}
}, [props.content]);
const onEditorPaste = useCallback(async (event: any = null) => {
const resourceMds = await getResourcesFromPasteEvent(event);
if (!resourceMds.length) return;
if (editorRef.current) {
editorRef.current.insertText(resourceMds.join('\n'));
}
}, []);
const editorCutText = useCallback(() => {
if (editorRef.current) {
const selections = editorRef.current.getSelections();
if (selections.length > 0 && selections[0]) {
clipboard.writeText(selections[0]);
// Easy way to wipe out just the first selection
selections[0] = '';
editorRef.current.replaceSelections(selections);
} else {
const cursor = editorRef.current.getCursor();
const line = editorRef.current.getLine(cursor.line);
clipboard.writeText(`${line}\n`);
const startLine = editorRef.current.getCursor('head');
startLine.ch = 0;
const endLine = {
line: startLine.line + 1,
ch: 0,
};
editorRef.current.replaceRange('', startLine, endLine);
}
}
}, []);
const editorCopyText = useCallback(() => {
if (editorRef.current) {
const selections = editorRef.current.getSelections();
// Handle the case when there is a selection - copy the selection to the clipboard
// When there is no selection, the selection array contains an empty string.
if (selections.length > 0 && selections[0]) {
clipboard.writeText(selections[0]);
} else {
// This is the case when there is no selection - copy the current line to the clipboard
const cursor = editorRef.current.getCursor();
const line = editorRef.current.getLine(cursor.line);
clipboard.writeText(line);
}
}
}, []);
const editorPasteText = useCallback(async () => {
if (editorRef.current) {
const modifiedMd = await Note.replaceResourceExternalToInternalLinks(clipboard.readText(), { useAbsolutePaths: true });
editorRef.current.insertText(modifiedMd);
}
}, []);
const editorPaste = useCallback(() => {
const clipboardText = clipboard.readText();
if (clipboardText) {
void editorPasteText();
} else {
// To handle pasting images
void onEditorPaste();
}
}, [editorPasteText, onEditorPaste]);
const commands = useEditorCommands({
webviewRef,
editorRef,
selectionRange,
editorCopyText, editorCutText, editorPaste,
editorContent: props.content,
visiblePanes: props.visiblePanes,
contentMarkupLanguage: props.contentMarkupLanguage,
});
useImperativeHandle(ref, () => {
return {
content: () => props.content,
resetScroll: () => {
resetScroll();
},
scrollTo: (options: ScrollOptions) => {
if (options.type === ScrollOptionTypes.Hash) {
if (!webviewRef.current) return;
webviewRef.current.send('scrollToHash', options.value as string);
} else if (options.type === ScrollOptionTypes.Percent) {
const percent = options.value as number;
setEditorPercentScroll(percent);
setViewerPercentScroll(percent);
} else {
throw new Error(`Unsupported scroll options: ${options.type}`);
}
},
supportsCommand: (name: string) => {
return name in commands || editorRef.current.supportsCommand(name);
},
execCommand: async (cmd: EditorCommand) => {
if (!editorRef.current) return false;
logger.debug('execCommand', cmd);
let commandOutput = null;
if (cmd.name in commands) {
commandOutput = (commands as any)[cmd.name](cmd.value);
} else if (editorRef.current.supportsCommand(cmd.name)) {
commandOutput = editorRef.current.execCommand(cmd.name);
} else if (editorRef.current.supportsJoplinCommand(cmd.name)) {
commandOutput = editorRef.current.execJoplinCommand(cmd.name);
} else {
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
}
return commandOutput;
},
};
}, [props.content, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
}, []);
const webview_ipcMessage = useWebviewIpcMessage({
editorRef,
setEditorPercentScroll,
getLineScrollPercent,
content: props.content,
onMessage: props.onMessage,
});
useEffect(() => {
let cancelled = false;
// When a new note is loaded (contentKey is different), we want the note to be displayed
// right away. However once that's done, we put a small delay so that the view is not
// being constantly updated while the user changes the note.
const interval = renderedBodyContentKey !== props.contentKey ? 0 : 500;
const timeoutId = shim.setTimeout(async () => {
let bodyToRender = props.content;
if (!bodyToRender.trim() && props.visiblePanes.indexOf('viewer') >= 0 && props.visiblePanes.indexOf('editor') < 0) {
// Fixes https://github.com/laurent22/joplin/issues/217
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
}
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({
resourceInfos: props.resourceInfos,
contentMaxWidth: props.contentMaxWidth,
mapsToLine: true,
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
useCustomPdfViewer: props.useCustomPdfViewer,
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
}));
if (cancelled) return;
setRenderedBody(result);
// Since we set `renderedBodyContentKey` here, it means this effect is going to
// be triggered again, but that's hard to avoid and the second call would be cheap
// anyway since the renderered markdown is cached by MdToHtml. We could use a ref
// to avoid this, but a second rendering might still happens anyway to render images,
// resources, or for other reasons. So it's best to focus on making any second call
// to this effect as cheap as possible with caching, etc.
setRenderedBodyContentKey(props.contentKey);
}, interval);
return () => {
cancelled = true;
shim.clearTimeout(timeoutId);
};
}, [
props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage,
props.visiblePanes, props.resourceInfos, props.markupToHtml, props.contentMaxWidth,
props.noteId, props.useCustomPdfViewer,
]);
useEffect(() => {
if (!webviewReady) return;
let lineCount = 0;
if (editorRef.current) {
lineCount = editorRef.current.editor.state.doc.lines;
}
const options: any = {
pluginAssets: renderedBody.pluginAssets,
downloadResources: Setting.value('sync.resourceDownloadMode'),
markupLineCount: lineCount,
};
// It seems when there's an error immediately when the component is
// mounted, webviewReady might be true, but webviewRef.current will be
// undefined. Maybe due to the error boundary that unmount components.
// Since we can't do much about it we just print an error.
if (webviewRef.current) {
// To keep consistency among CodeMirror's editing and scroll percents
// of Editor and Viewer.
const percent = getLineScrollPercent();
setEditorPercentScroll(percent);
options.percent = percent;
webviewRef.current.send('setHtml', renderedBody.html, options);
} else {
console.error('Trying to set HTML on an undefined webview ref');
}
}, [renderedBody, webviewReady, getLineScrollPercent, setEditorPercentScroll]);
const cellEditorStyle = useMemo(() => {
const output = { ...styles.cellEditor };
if (!props.visiblePanes.includes('editor')) {
output.display = 'none'; // Seems to work fine since the refactoring
}
return output;
}, [styles.cellEditor, props.visiblePanes]);
const cellViewerStyle = useMemo(() => {
const output = { ...styles.cellViewer };
if (!props.visiblePanes.includes('viewer')) {
// Note: setting webview.display to "none" is currently not supported due
// to this bug: https://github.com/electron/electron/issues/8277
// So instead setting the width 0.
output.width = 1;
output.maxWidth = 1;
} else if (!props.visiblePanes.includes('editor')) {
output.borderLeftStyle = 'none';
}
return output;
}, [styles.cellViewer, props.visiblePanes]);
// Disable this effect to fix this:
//
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
// to automatically focus the editor when the layout changes. The workaround
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
//
// On the other hand, if we automatically focus the editor, and the user
// does not want this, there's no workaround, so it's better to have this
// disabled.
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
// useEffect(() => {
// if (!editorRef.current) return;
// // Anytime the user toggles the visible panes AND the editor is visible as a result
// // we should focus the editor
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
// // an editor interaction so users should expect the editor to be focused
// if (editorPaneVisible) {
// editorRef.current.focus();
// }
// }, [editorPaneVisible]);
useContextMenu({
plugins: props.plugins,
editorCutText, editorCopyText, editorPaste,
editorRef,
editorClassName: 'cm-editor',
});
const onEditorEvent = useCallback((event: EditorEvent) => {
if (event.kind === EditorEventType.Scroll) {
editor_scroll();
} else if (event.kind === EditorEventType.Change) {
codeMirror_change(event.value);
} else if (event.kind === EditorEventType.SelectionRangeChange) {
setSelectionRange({ from: event.from, to: event.to });
}
}, [editor_scroll, codeMirror_change]);
const editorSettings = useMemo((): EditorSettings => {
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
let keyboardMode = EditorKeymap.Default;
if (props.keyboardMode === 'vim') {
keyboardMode = EditorKeymap.Vim;
} else if (props.keyboardMode === 'emacs') {
keyboardMode = EditorKeymap.Emacs;
}
return {
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
readOnly: props.disabled || props.visiblePanes.indexOf('editor') < 0,
katexEnabled: Setting.value('markdown.plugin.katex'),
themeData: {
...styles.globalTheme,
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
},
automatchBraces: Setting.value('editor.autoMatchingBraces'),
useExternalSearch: false,
ignoreModifiers: true,
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
keymap: keyboardMode,
indentWithTabs: true,
};
}, [
props.contentMarkupLanguage, props.disabled, props.visiblePanes,
props.keyboardMode, styles.globalTheme,
]);
// Update the editor's value
useEffect(() => {
if (editorRef.current?.updateBody(props.content)) {
editorRef.current?.clearHistory();
}
}, [props.content]);
const renderEditor = () => {
return (
<div style={cellEditorStyle}>
<Editor
style={styles.editor}
initialText={props.content}
ref={editorRef}
settings={editorSettings}
pluginStates={props.plugins}
onEvent={onEditorEvent}
onLogMessage={logDebug}
onEditorPaste={onEditorPaste}
/>
</div>
);
};
const renderViewer = () => {
return (
<div style={cellViewerStyle}>
<NoteTextViewer
ref={webviewRef}
themeId={props.themeId}
viewerStyle={styles.viewer}
onIpcMessage={webview_ipcMessage}
onDomReady={webview_domReady}
contentMaxWidth={props.contentMaxWidth}
/>
</div>
);
};
return (
<ErrorBoundary message="The text editor encountered a fatal error and could not continue. The error might be due to a plugin, so please try to disable some of them and try again.">
<div style={styles.root} ref={rootRef}>
<div style={styles.rowToolbar}>
<Toolbar themeId={props.themeId}/>
{props.noteToolbar}
</div>
<div style={styles.rowEditorViewer}>
{renderEditor()}
{renderViewer()}
</div>
</div>
</ErrorBoundary>
);
};
export default forwardRef(CodeMirror);

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import { ForwardedRef } from 'react';
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { EditorProps, LogMessageCallback, OnEventCallback, PluginData } from '@joplin/editor/types';
import createEditor from '@joplin/editor/CodeMirror/createEditor';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import shim from '@joplin/lib/shim';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import setupVim from '../utils/setupVim';
interface Props extends EditorProps {
style: React.CSSProperties;
pluginStates: PluginStates;
onEditorPaste: (event: Event)=> void;
}
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
const editorContainerRef = useRef<HTMLDivElement>();
const [editor, setEditor] = useState<CodeMirrorControl|null>(null);
// The editor will only be created once, so callbacks that could
// change need to be stored as references.
const onEventRef = useRef<OnEventCallback>(props.onEvent);
const onLogMessageRef = useRef<LogMessageCallback>(props.onLogMessage);
useEffect(() => {
onEventRef.current = props.onEvent;
onLogMessageRef.current = props.onLogMessage;
}, [props.onEvent, props.onLogMessage]);
useEffect(() => {
if (!editor) {
return () => {};
}
const pasteEventHandler = (_editor: any, event: Event) => {
props.onEditorPaste(event);
};
editor.on('paste', pasteEventHandler);
return () => {
editor.off('paste', pasteEventHandler);
};
}, [editor, props.onEditorPaste]);
useImperativeHandle(ref, () => {
return editor;
}, [editor]);
useEffect(() => {
if (!editor) {
return;
}
const plugins: PluginData[] = [];
for (const pluginId in props.pluginStates) {
const pluginState = props.pluginStates[pluginId];
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
for (const contentScript of codeMirrorContentScripts) {
plugins.push({
pluginId,
contentScriptId: contentScript.id,
contentScriptJs: () => shim.fsDriver().readFile(contentScript.path),
postMessageHandler: (message: any) => {
const plugin = PluginService.instance().pluginById(pluginId);
return plugin.emitContentScriptMessage(contentScript.id, message);
},
});
}
}
void editor.setPlugins(plugins);
}, [editor, props.pluginStates]);
useEffect(() => {
if (!editorContainerRef.current) return () => {};
const editorProps: EditorProps = {
...props,
onEvent: event => onEventRef.current(event),
onLogMessage: message => onLogMessageRef.current(message),
};
const editor = createEditor(editorContainerRef.current, editorProps);
editor.addStyles({
'.cm-scroller': { overflow: 'auto' },
});
setEditor(editor);
return () => {
editor.remove();
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
}, []);
useEffect(() => {
editor?.updateSettings(props.settings);
}, [props.settings, editor]);
useEffect(() => {
if (!editor) {
return;
}
setupVim(editor);
}, [editor]);
return (
<div
style={props.style}
ref={editorContainerRef}
></div>
);
};
export default forwardRef(Editor);

View File

@@ -0,0 +1,138 @@
import { RefObject, useMemo } from 'react';
import { CommandValue } from '../../../utils/types';
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
import { _ } from '@joplin/lib/locale';
import dialogs from '../../../../dialogs';
import { EditorCommandType } from '@joplin/editor/types';
import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer';
const logger = Logger.create('CodeMirror 6 commands');
const wrapSelectionWithStrings = (editor: CodeMirrorControl, string1: string, string2 = '', defaultText = '') => {
if (editor.somethingSelected()) {
editor.wrapSelections(string1, string2);
} else {
editor.wrapSelections(string1 + defaultText, string2);
// Now select the default text so the user can replace it
const selections = editor.listSelections();
const newSelections = [];
for (let i = 0; i < selections.length; i++) {
const s = selections[i];
const anchor = { line: s.anchor.line, ch: s.anchor.ch + string1.length };
const head = { line: s.head.line, ch: s.head.ch - string2.length };
newSelections.push({ anchor: anchor, head: head });
}
editor.setSelections(newSelections);
}
};
interface Props {
webviewRef: RefObject<any>;
editorRef: RefObject<CodeMirrorControl>;
editorContent: string;
editorCutText(): void;
editorCopyText(): void;
editorPaste(): void;
selectionRange: { from: number; to: number };
visiblePanes: string[];
contentMarkupLanguage: MarkupLanguage;
}
const useEditorCommands = (props: Props) => {
const editorRef = props.editorRef;
return useMemo(() => {
const selectedText = () => {
if (!editorRef.current) return '';
return editorRef.current.getSelection();
};
return {
dropItems: async (cmd: any) => {
if (cmd.type === 'notes') {
editorRef.current.insertText(cmd.markdownTags.join('\n'));
} else if (cmd.type === 'files') {
const pos = props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage });
editorRef.current.updateBody(newBody);
} else {
logger.warn('CodeMirror: unsupported drop item: ', cmd);
}
},
selectedText: () => {
return selectedText();
},
selectedHtml: () => {
return selectedText();
},
replaceSelection: (value: string) => {
return editorRef.current.insertText(value);
},
textCopy: () => {
props.editorCopyText();
},
textCut: () => {
props.editorCutText();
},
textPaste: () => {
props.editorPaste();
},
textSelectAll: () => {
return editorRef.current.execCommand(EditorCommandType.SelectAll);
},
textLink: async () => {
const url = await dialogs.prompt(_('Insert Hyperlink'));
editorRef.current.focus();
if (url) wrapSelectionWithStrings(editorRef.current, '[', `](${url})`);
},
insertText: (value: any) => editorRef.current.insertText(value),
attachFile: async () => {
const newBody = await commandAttachFileToBody(
props.editorContent, null, { position: props.selectionRange.from, markupLanguage: props.contentMarkupLanguage },
);
if (newBody) {
editorRef.current.updateBody(newBody);
}
},
textHorizontalRule: () => editorRef.current.insertText('* * *'),
'editor.execCommand': (value: CommandValue) => {
if (!('args' in value)) value.args = [];
if ((editorRef.current as any)[value.name]) {
const result = (editorRef.current as any)[value.name](...value.args);
return result;
} else if (editorRef.current.commandExists(value.name)) {
const result = editorRef.current.execCommand(value.name);
return result;
} else {
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
}
},
'editor.focus': () => {
if (props.visiblePanes.indexOf('editor') >= 0) {
editorRef.current.editor.focus();
} else {
// If we just call focus() then the iframe is focused,
// but not its content, such that scrolling up / down
// with arrow keys fails
props.webviewRef.current.send('focus');
}
},
search: () => {
editorRef.current.execCommand(EditorCommandType.ShowSearch);
},
};
}, [
props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste,
props.selectionRange,
props.contentMarkupLanguage,
props.webviewRef, editorRef,
]);
};
export default useEditorCommands;

View File

@@ -0,0 +1,56 @@
// Used in safe mode
import * as React from 'react';
import { ForwardedRef } from 'react';
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
import { NoteBodyEditorProps, NoteBodyEditorRef } from '../../utils/types';
const PlainEditor = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
const editorRef = useRef<HTMLTextAreaElement>();
useImperativeHandle(ref, () => {
return {
content: () => editorRef.current?.value ?? '',
resetScroll: () => {
editorRef.current.scrollTop = 0;
},
scrollTo: () => {
// Not supported
},
supportsCommand: _name => {
return false;
},
execCommand: async _command => {
// Not supported
},
};
}, []);
useEffect(() => {
if (!editorRef.current) return;
if (editorRef.current.value !== props.content) {
editorRef.current.value = props.content;
}
}, [props.content]);
const onChange = useCallback((event: any) => {
props.onChange({ changeId: null, content: event.target.value });
}, [props.onChange]);
return (
<div style={props.style}>
<textarea
ref={editorRef}
style={{ width: '100%', height: '100%' }}
defaultValue={props.content}
onChange={onChange}
/>
</div>
);
};
export default forwardRef(PlainEditor);

View File

@@ -1,50 +0,0 @@
// Kept only for reference
import * as React from 'react';
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
export interface OnChangeEvent {
changeId: number,
content: any,
}
interface PlainEditorProps {
style: any,
onChange(event: OnChangeEvent): void,
onWillChange(event:any): void,
markupToHtml: Function,
disabled: boolean,
}
const PlainEditor = (props:PlainEditorProps, ref:any) => {
const editorRef = useRef<any>();
useImperativeHandle(ref, () => {
return {
content: () => '',
};
}, []);
useEffect(() => {
if (!editorRef.current) return;
editorRef.current.value = props.defaultEditorState.value;
}, [props.defaultEditorState]);
const onChange = useCallback((event:any) => {
props.onChange({ changeId: null, content: event.target.value });
}, [props.onWillChange, props.onChange]);
return (
<div style={props.style}>
<textarea
ref={editorRef}
style={{ width: '100%', height: '100%' }}
defaultValue={props.defaultEditorState.value}
onChange={onChange}
/>;
</div>
);
};
export default forwardRef(PlainEditor);

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
import { connect } from 'react-redux';
import MultiNoteActions from '../MultiNoteActions';
import { htmlToMarkdown, formNoteToNote } from './utils';
@@ -15,7 +14,7 @@ import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import useEffectiveNoteId from './utils/useEffectiveNoteId';
import useFolder from './utils/useFolder';
import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
@@ -46,6 +45,9 @@ import { ModelType } from '@joplin/lib/BaseModel';
import BaseItem from '@joplin/lib/models/BaseItem';
import { ErrorCode } from '@joplin/lib/errors';
import ItemChange from '@joplin/lib/models/ItemChange';
import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
const commands = [
require('./commands/showRevisions'),
@@ -59,7 +61,7 @@ function NoteEditor(props: NoteEditorProps) {
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions>(null);
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
const editorRef = useRef<any>();
const editorRef = useRef<NoteBodyEditorRef>();
const titleInputRef = useRef<any>();
const isMountedRef = useRef(true);
const noteSearchBarRef = useRef(null);
@@ -380,7 +382,7 @@ function NoteEditor(props: NoteEditorProps) {
};
}, [setShowRevisions]);
const onScroll = useCallback((event: any) => {
const onScroll = useCallback((event: { percent: number }) => {
props.dispatch({
type: 'EDITOR_SCROLL_PERCENT_SET',
// In callbacks of setTimeout()/setInterval(), props/state cannot be used
@@ -389,8 +391,7 @@ function NoteEditor(props: NoteEditorProps) {
noteId: formNoteRef.current.id,
percent: event.percent,
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.dispatch, formNote]);
}, [props.dispatch]);
function renderNoNotes(rootStyle: any) {
const emptyDivStyle = {
@@ -461,14 +462,18 @@ function NoteEditor(props: NoteEditorProps) {
if (props.bodyEditor === 'TinyMCE') {
editor = <TinyMCE {...editorProps}/>;
} else if (props.bodyEditor === 'PlainText') {
editor = <PlainEditor {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror') {
editor = <CodeMirror {...editorProps}/>;
editor = <CodeMirror5 {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror6') {
editor = <CodeMirror6 {...editorProps}/>;
} else {
throw new Error(`Invalid editor: ${props.bodyEditor}`);
}
const onRichTextReadMoreLinkClick = useCallback(() => {
bridge().openExternal('https://joplinapp.org/rich_text_editor');
bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
}, []);
const onRichTextDismissLinkClick = useCallback(() => {
@@ -602,7 +607,7 @@ function NoteEditor(props: NoteEditorProps) {
disabled={isReadOnly}
/>
{renderSearchInfo()}
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%' }}>
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%', minHeight: '0' }}>
{editor}
</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>

View File

@@ -109,7 +109,7 @@ export default function NoteTitleBar(props: Props) {
className="title-input"
type="text"
ref={props.titleInputRef}
placeholder={props.isProvisional ? _('Creating new %s...', props.noteIsTodo ? _('to-do') : _('note')) : ''}
placeholder={props.isProvisional ? (props.noteIsTodo ? _('Creating new to-do...') : _('Creating new note...')) : ''}
style={styles.titleInput}
readOnly={props.disabled}
onChange={props.onTitleChange}

View File

@@ -2,9 +2,14 @@ import { CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands';
const workWithHtmlNotes = [
'attachFile',
];
export const enabledCondition = (commandName: string) => {
const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName);
return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected && noteIsMarkdown && !noteIsReadOnly`;
const noteMustBeMarkdown = !workWithHtmlNotes.includes(commandName);
return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected ${noteMustBeMarkdown ? '&& noteIsMarkdown' : ''} && !noteIsReadOnly`;
};
const declarations: CommandDeclaration[] = [

View File

@@ -9,6 +9,7 @@ import htmlUtils from '@joplin/lib/htmlUtils';
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
import Logger from '@joplin/utils/Logger';
import { fileUriToPath } from '@joplin/utils/url';
import { MarkupLanguage } from '@joplin/renderer';
const joplinRendererUtils = require('@joplin/renderer').utils;
const { clipboard } = require('electron');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
@@ -62,6 +63,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
options = {
createFileURL: false,
position: 0,
markupLanguage: MarkupLanguage.Markdown,
...options,
};
@@ -79,6 +81,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
createFileURL: options.createFileURL,
resizeLargeImages: Setting.value('imageResizing'),
markupLanguage: options.markupLanguage,
});
if (!newBody) {

View File

@@ -48,6 +48,15 @@ export interface NoteEditorProps {
syncUserId: string;
}
export interface NoteBodyEditorRef {
content(): string|Promise<string>;
resetScroll(): void;
scrollTo(options: ScrollOptions): void;
supportsCommand(name: string): boolean;
execCommand(command: CommandValue): Promise<void>;
}
export interface NoteBodyEditorProps {
style: any;
ref: any;
@@ -59,7 +68,7 @@ export interface NoteBodyEditorProps {
onChange(event: OnChangeEvent): void;
onWillChange(event: any): void;
onMessage(event: any): void;
onScroll(event: any): void;
onScroll(event: { percent: number }): void;
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
htmlToMarkdown: Function;

View File

@@ -3,6 +3,16 @@ import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/
import { renderHook } from '@testing-library/react-hooks';
import useFormNote, { HookDependencies } from './useFormNote';
const defaultFormNoteProps: HookDependencies = {
syncStarted: false,
decryptionStarted: false,
noteId: '',
isProvisional: false,
titleInputRef: null,
editorRef: null,
onBeforeLoad: ()=>{},
onAfterLoad: ()=>{},
};
describe('useFormNote', () => {
beforeEach(async () => {
@@ -15,14 +25,10 @@ describe('useFormNote', () => {
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
return {
...defaultFormNoteProps,
syncStarted,
decryptionStarted,
noteId: testNote.id,
isProvisional: false,
titleInputRef: null,
editorRef: null,
onBeforeLoad: ()=>{},
onAfterLoad: ()=>{},
};
};
@@ -70,4 +76,37 @@ describe('useFormNote', () => {
});
});
});
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
// it('should reload the note when it is changed outside of the editor', async () => {
// const note = await Note.save({ title: 'Test Note!' });
// const makeFormNoteProps = (dbNote: DbNote): HookDependencies => {
// return {
// ...defaultFormNoteProps,
// noteId: note.id,
// dbNote,
// };
// };
// const formNote = renderHook(props => useFormNote(props), {
// initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }),
// });
// await formNote.waitFor(() => {
// expect(formNote.result.current.formNote.title).toBe('Test Note!');
// });
// // Simulate the note being modified outside the editor
// const modifiedNote = await Note.save({ id: note.id, title: 'Modified' });
// // NoteEditor then would update `dbNote`
// formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time }));
// await formNote.waitFor(() => {
// expect(formNote.result.current.formNote.title).toBe('Modified');
// });
// });
});

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { FormNote, ScrollOptionTypes } from './types';
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
@@ -12,6 +12,8 @@ const commandsWithDependencies = [
require('../commands/pasteAsText'),
];
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
interface HookDependencies {
formNote: FormNote;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@@ -19,16 +21,18 @@ interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
dispatch: Function;
noteSearchBarRef: any;
editorRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
titleInputRef: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
saveNoteAndWait: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
setFormNote: Function;
setFormNote: SetFormNoteCallback;
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime {
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
): CommandRuntime {
return {
execute: async (_context: CommandContext, ...args: any[]) => {
if (!editorRef.current) {

View File

@@ -271,7 +271,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
controlComp = (
<Datetime
ref="editField"
defaultValue={value}
initialValue={value}
dateFormat={time.dateFormat()}
timeFormat={time.timeFormat()}
inputProps={{

View File

@@ -67,5 +67,6 @@ export default function() {
'switchProfile2',
'switchProfile3',
'pasteAsText',
'showNoteProperties',
];
}

View File

@@ -0,0 +1,17 @@
# Integration tests
The integration tests in this directory can be run with `yarn playwright test`.
- Tests use a `test-profile` directory that should be re-created before every test.
- Only one Electron application should be instantiated per test file.
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
# References
The following sources are helpful for designing and implementing Electron integration tests
with Playwright:
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89)
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts).
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
- [Playwright best practices](https://playwright.dev/docs/best-practices)

View File

@@ -0,0 +1,146 @@
import { test, expect } from './util/test';
import MainScreen from './models/MainScreen';
import activateMainMenuItem from './util/activateMainMenuItem';
import SettingsScreen from './models/SettingsScreen';
import { _electron as electron } from '@playwright/test';
import { writeFile } from 'fs-extra';
import { join } from 'path';
test.describe('main', () => {
test('app should launch', async ({ mainWindow }) => {
// A window should open with the correct title
expect(await mainWindow.title()).toMatch(/^Joplin/);
const mainPage = new MainScreen(mainWindow);
await mainPage.waitFor();
});
test('should be able to create and edit a new note', async ({ mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.newNoteButton.click();
const editor = mainScreen.noteEditor;
await editor.waitFor();
// Wait for the title input to have the correct placeholder
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
// Fill the title
await editor.noteTitleInput.click();
await editor.noteTitleInput.fill('Test note');
// Note list should contain the new note
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
// Focus the editor
await editor.codeMirrorEditor.click();
// Type some text
await mainWindow.keyboard.type('# Test note!');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('New note content!');
// Should render
const viewerFrame = editor.getNoteViewerIframe();
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
});
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
// Sort order buttons should be visible by default
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
// Open settings (check both labels so that this works on MacOS)
expect(
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'),
).toBe(true);
// Should be on the settings screen
const settingsScreen = new SettingsScreen(mainWindow);
await settingsScreen.waitFor();
// Open the appearance tab
await settingsScreen.appearanceTabButton.click();
// Find the sort order visible checkbox
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
await expect(sortOrderVisibleCheckbox).toBeChecked();
await sortOrderVisibleCheckbox.click();
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
// Save settings & close
await settingsScreen.okayButton.click();
await mainScreen.waitFor();
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
});
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
// Mock openExternal
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
return new Promise<string>(resolve => {
const openExternal = async (url: string) => {
resolve(url);
};
shell.openExternal = openExternal;
});
});
// Create a test link
const testLinkTitle = 'This is a test link!';
const linkHref = 'https://joplinapp.org/';
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
const testLink = document.createElement('a');
testLink.textContent = testLinkTitle;
testLink.onclick = () => {
// We need to navigate by setting location.href -- clicking on a link
// directly within the main window (i.e. not in a PDF viewer) doesn't
// navigate.
location.href = linkHref;
};
testLink.href = '#';
// Display on top of everything
testLink.style.zIndex = '99999';
testLink.style.position = 'fixed';
testLink.style.top = '0';
testLink.style.left = '0';
document.body.appendChild(testLink);
}, { testLinkTitle, linkHref });
const testLink = mainWindow.getByText(testLinkTitle);
await expect(testLink).toBeVisible();
await testLink.click({ noWaitAfter: true });
expect(await nextExternalUrlPromise).toBe(linkHref);
});
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');
// We need to write to the force-safe-mode file before opening the Electron app.
// Open the app ourselves:
const startupArgs = [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const electronApp = await electron.launch({ args: startupArgs });
const mainWindow = await electronApp.firstWindow();
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
await safeModeDisableLink.waitFor();
await expect(safeModeDisableLink).toBeInViewport();
await electronApp.close();
});
});

View File

@@ -0,0 +1,20 @@
import { Page, Locator } from '@playwright/test';
import NoteEditorScreen from './NoteEditorScreen';
export default class MainScreen {
public readonly newNoteButton: Locator;
public readonly noteListContainer: Locator;
public readonly noteEditor: NoteEditorScreen;
public constructor(page: Page) {
this.newNoteButton = page.locator('.new-note-button');
this.noteListContainer = page.locator('.rli-noteList');
this.noteEditor = new NoteEditorScreen(page);
}
public async waitFor() {
await this.newNoteButton.waitFor();
await this.noteEditor.waitFor();
await this.noteListContainer.waitFor();
}
}

View File

@@ -0,0 +1,26 @@
import { Locator, Page } from '@playwright/test';
export default class NoteEditorPage {
public readonly codeMirrorEditor: Locator;
public readonly noteTitleInput: Locator;
private readonly containerLocator: Locator;
public constructor(private readonly page: Page) {
this.containerLocator = page.locator('.rli-editor');
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
this.noteTitleInput = this.containerLocator.locator('.title-input');
}
public getNoteViewerIframe() {
// The note viewer can change content when the note re-renders. As such,
// a new locator needs to be created after re-renders (and this can't be a
// static property).
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
}
public async waitFor() {
await this.codeMirrorEditor.waitFor();
await this.noteTitleInput.waitFor();
}
}

View File

@@ -0,0 +1,17 @@
import { Page, Locator } from '@playwright/test';
export default class SettingsScreen {
public readonly okayButton: Locator;
public readonly appearanceTabButton: Locator;
public constructor(page: Page) {
this.okayButton = page.locator('button', { hasText: 'OK' });
this.appearanceTabButton = page.getByText('Appearance');
}
public async waitFor() {
await this.okayButton.waitFor();
await this.appearanceTabButton.waitFor();
}
}

View File

@@ -0,0 +1,13 @@
#!/bin/sh
echo "Running desktop integration tests..."
export CI=true
if test "$RUNNER_OS" = "Linux" ; then
# The Ubuntu Github CI doesn't have a display server.
# Start a virtual one with xvfb-run.
xvfb-run -- yarn run playwright test
else
yarn run playwright test
fi

View File

@@ -0,0 +1,36 @@
import type { ElectronApplication } from '@playwright/test';
import type { MenuItem } from 'electron';
// Roughly based on
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]).
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => {
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => {
const activateItemInSubmenu = (submenu: MenuItem[]) => {
for (const item of submenu) {
if (item.label === menuItemLabel && item.visible) {
// Found!
item.click();
return true;
} else if (item.submenu) {
const foundItem = activateItemInSubmenu(item.submenu.items);
if (foundItem) {
return true;
}
}
}
// No item found
return false;
};
const appMenu = Menu.getApplicationMenu();
return activateItemInSubmenu(appMenu.items);
}, menuItemLabel);
};
export default activateMainMenuItem;

View File

@@ -0,0 +1,53 @@
import { resolve, join, dirname } from 'path';
import { remove, mkdirp } from 'fs-extra';
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
import uuid from '@joplin/lib/uuid';
type JoplinFixtures = {
profileDirectory: string;
electronApp: ElectronApplication;
mainWindow: Page;
};
// A custom fixture that loads an electron app. See
// https://playwright.dev/docs/test-fixtures
export const test = base.extend<JoplinFixtures>({
// Playwright fails if we don't use the object destructuring
// pattern in the first argument.
//
// See https://github.com/microsoft/playwright/issues/8798
//
// eslint-disable-next-line no-empty-pattern
profileDirectory: async ({ }, use) => {
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
const profileSubdir = join(profilePath, uuid.createNano());
await mkdirp(profileSubdir);
await use(profileSubdir);
await remove(profileSubdir);
},
electronApp: async ({ profileDirectory }, use) => {
const startupArgs = [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const electronApp = await electron.launch({ args: startupArgs });
await use(electronApp);
await electronApp.firstWindow();
await electronApp.close();
},
mainWindow: async ({ electronApp }, use) => {
const window = await electronApp.firstWindow();
await use(window);
},
});
export { expect } from '@playwright/test';

View File

@@ -31,123 +31,122 @@ const React = require('react');
const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default;
// Security: If we attempt to navigate away from the root HTML page, it's likely because
// of an improperly sanitized link. Prevent this by closing the window before we can
// navigate away.
window.onbeforeunload = () => {
window.close();
};
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
const output = {};
const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn'];
for (const fnName of fnNames) {
if (fnName === 'warn') {
output.warn = function(...text) {
const s = [...text].join('');
// React spams the console with walls of warnings even outside of strict mode, and even after having renamed
// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them...
if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return;
if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return;
oldConsole.warn(...text);
};
} else {
output[fnName] = function(...text) {
return oldConsole[fnName](...text);
};
const main = async () => {
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
const output = {};
const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn'];
for (const fnName of fnNames) {
if (fnName === 'warn') {
output.warn = function(...text) {
const s = [...text].join('');
// React spams the console with walls of warnings even outside of strict mode, and even after having renamed
// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them...
if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return;
if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return;
oldConsole.warn(...text);
};
} else {
output[fnName] = function(...text) {
return oldConsole[fnName](...text);
};
}
}
}
return output;
}(window.console);
return output;
}(window.console);
window.console = newConsole;
}
window.console = newConsole;
}
// eslint-disable-next-line no-console
console.info(`Environment: ${bridge().env()}`);
// eslint-disable-next-line no-console
console.info(`Environment: ${bridge().env()}`);
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`);
Setting.setConstant('appType', 'desktop');
Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`);
Setting.setConstant('appType', 'desktop');
// eslint-disable-next-line no-console
console.info(`appId: ${Setting.value('appId')}`);
// eslint-disable-next-line no-console
console.info(`appType: ${Setting.value('appType')}`);
// eslint-disable-next-line no-console
console.info(`appId: ${Setting.value('appId')}`);
// eslint-disable-next-line no-console
console.info(`appType: ${Setting.value('appType')}`);
let keytar;
try {
keytar = shim.platformSupportsKeyChain() ? require('keytar') : null;
} catch (error) {
console.error('Cannot load keytar - keychain support will be disabled', error);
keytar = null;
}
let keytar;
try {
keytar = shim.platformSupportsKeyChain() ? require('keytar') : null;
} catch (error) {
console.error('Cannot load keytar - keychain support will be disabled', error);
keytar = null;
}
function appVersion() {
const p = require('./packageInfo.js');
return p.version;
}
function appVersion() {
const p = require('./packageInfo.js');
return p.version;
}
shimInit({
keytar,
React,
appVersion,
electronBridge: bridge(),
nodeSqlite,
});
shimInit({
keytar,
React,
appVersion,
electronBridge: bridge(),
nodeSqlite,
});
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
// Disable middle-click (which would open a new browser window, but we don't want this)
document.addEventListener('auxclick', event => event.preventDefault());
// Disable middle-click (which would open a new browser window, but we don't want this)
document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
event.preventDefault();
});
event.preventDefault();
});
const logger = new Logger();
Logger.initializeGlobalLogger(logger);
initLib(logger);
const logger = new Logger();
Logger.initializeGlobalLogger(logger);
initLib(logger);
app().start(bridge().processArgv()).then((result) => {
if (!result || !result.action) {
const startResult = await app().start(bridge().processArgv());
if (!startResult || !startResult.action) {
require('./gui/Root');
} else if (result.action === 'upgradeSyncTarget') {
} else if (startResult.action === 'upgradeSyncTarget') {
require('./gui/Root_UpgradeSyncTarget');
}
}).catch((error) => {
const env = bridge().env();
};
main().catch((error) => {
const env = bridge().env();
console.error(error);
let errorMessage;
if (error.code === 'flagError') {
bridge().showErrorMessageBox(error.message);
errorMessage = error.message;
} else {
// If something goes wrong at this stage we don't have a console or a log file
// so display the error in a message box.
@@ -156,13 +155,12 @@ app().start(bridge().processArgv()).then((result) => {
if (error.lineNumber) msg.push(error.lineNumber);
if (error.stack) msg.push(error.stack);
if (env === 'dev') {
console.error(error);
} else {
bridge().showErrorMessageBox(msg.join('\n\n'));
}
errorMessage = msg.join('\n\n');
}
// In dev, we leave the app open as debug statements in the console can be useful
if (env !== 'dev') bridge().electronApp().exit(1);
// In dev, we give the option to leave the app open as debug statements in the
// console can be useful
const canIgnore = env === 'dev';
bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
});

View File

@@ -40,18 +40,17 @@ a {
}
::-webkit-scrollbar-thumb {
background: rgba(150, 149, 149, 0.4);
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(223, 223, 223, 0.6);
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
}
.fade_out {
-webkit-transition: 0.15s;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.1",
"version": "2.13.4",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -14,7 +14,8 @@
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
"test": "jest",
"test-ci": "yarn test",
"test-ui": "playwright test",
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
},
"repository": {
@@ -116,22 +117,23 @@
"devDependencies": {
"@electron/rebuild": "3.3.0",
"@joplin/tools": "~2.13",
"@playwright/test": "1.38.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.12",
"@types/react": "18.2.21",
"@types/react-redux": "7.1.26",
"@types/styled-components": "5.1.26",
"electron": "25.3.1",
"electron-builder": "24.4.0",
"glob": "10.3.3",
"@types/jest": "29.5.5",
"@types/node": "18.17.19",
"@types/react": "18.2.31",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"electron": "25.9.1",
"electron-builder": "24.6.4",
"glob": "10.3.10",
"gulp": "4.0.2",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"js-sha512": "0.8.0",
"nan": "2.17.0",
"nan": "2.18.0",
"react-test-renderer": "18.2.0",
"typescript": "5.1.6"
"typescript": "5.2.2"
},
"optionalDependencies": {
"7zip-bin-linux": "^1.0.1",
@@ -140,17 +142,18 @@
},
"dependencies": {
"@electron/notarize": "2.1.0",
"@electron/remote": "2.0.10",
"@electron/remote": "2.0.12",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
"@joplin/editor": "~2.13",
"@joplin/lib": "~2.13",
"@joplin/renderer": "~2.13",
"@joplin/utils": "~2.13",
"@types/mustache": "4.2.2",
"@types/mustache": "4.2.3",
"async-mutex": "0.4.0",
"codemirror": "5.65.9",
"color": "3.2.1",
"compare-versions": "3.6.0",
"compare-versions": "6.1.0",
"countable": "3.0.1",
"debounce": "1.2.1",
"electron-window-state": "5.0.3",
@@ -171,8 +174,8 @@
"react": "18.2.0",
"react-datetime": "3.2.0",
"react-dom": "18.2.0",
"react-redux": "8.1.2",
"react-select": "5.7.4",
"react-redux": "8.1.3",
"react-select": "5.7.7",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
"redux": "4.2.1",

View File

@@ -0,0 +1,34 @@
import { defineConfig } from '@playwright/test';
// See https://playwright.dev/docs/test-configuration.
export default defineConfig({
testDir: './integration-tests',
// Only match .ts files (no compiled .js files)
testMatch: '*.spec.ts',
// Allow running tests in parallel (note: each Joplin instance
// is given its own profile directory).
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,
// Retry on CI only
retries: process.env.CI ? 2 : 0,
// Opt out of parallel tests on CI
workers: process.env.CI ? 1 : undefined,
// Reporter to use. See https://playwright.dev/docs/test-reporters
reporter: process.env.CI ? 'line' : 'html',
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
use: {
// Base URL to use in actions like `await page.goto('/')`.
// baseURL: 'http://127.0.0.1:3000',
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
trace: 'on-first-retry',
},
});

View File

@@ -307,7 +307,7 @@ class Dialog extends React.PureComponent<Props, State> {
} else { // Note TITLE or BODY
listType = BaseModel.TYPE_NOTE;
searchQuery = gotoAnythingStyleQuery(this.state.query);
results = await SearchEngine.instance().search(searchQuery);
results = (await SearchEngine.instance().search(searchQuery)) as any[];
resultsInBody = !!results.find((row: any) => row.fields.includes('body'));

View File

@@ -12,8 +12,9 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC
...libStateToWhenClauseContext(state, options),
// UI elements
markdownEditorVisible: !!state.settings['editor.codeView'],
richTextEditorVisible: !state.settings['editor.codeView'],
markdownEditorVisible: !!state.settings['editor.codeView'] && !state.settings['isSafeMode'],
richTextEditorVisible: !state.settings['editor.codeView'] && !state.settings['isSafeMode'],
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,

View File

@@ -57,9 +57,12 @@ export default class PlatformImplementation extends BasePlatformImplementation {
this.joplin_ = {
views: {
dialogs: {
showMessageBox: async function(message: string) {
showMessageBox: async (message: string) => {
return bridge().showMessageBox(message);
},
showOpenDialog: async (options) => {
return bridge().showOpenDialog(options);
},
},
},
};

View File

@@ -5,17 +5,22 @@
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
TEMP_PATH=~/src/plugin-tests
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/note_list_renderer
NEED_COMPILING=0
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple
mkdir -p "$TEMP_PATH"
PLUGIN_NAME=$(echo "$PLUGIN_PATH" | awk -F/ '{print $NF}')
TEMP_PLUGIN_PATH="$TEMP_PATH/$PLUGIN_NAME"
if [[ $NEED_COMPILING == 1 ]]; then
mkdir -p "$TEMP_PATH"
PLUGIN_NAME=$(echo "$PLUGIN_PATH" | awk -F/ '{print $NF}')
TEMP_PLUGIN_PATH="$TEMP_PATH/$PLUGIN_NAME"
echo "Copying from: $PLUGIN_PATH"
echo "To: $TEMP_PLUGIN_PATH"
echo "Copying from: $PLUGIN_PATH"
echo "To: $TEMP_PLUGIN_PATH"
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
else
yarn start --dev-plugins "$PLUGIN_PATH"
fi
# Add eg "--profile $HOME/.config/joplindev-desktop-1" to test with a different profile

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