1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

120 Commits

Author SHA1 Message Date
Laurent Cozic
54f7a83789 Android 2.13.6 2023-11-09 19:45:45 +00:00
Laurent Cozic
1d04ec6b64 Desktop release v2.13.5 2023-11-09 19:31:11 +00:00
Laurent Cozic
99d93f0a85 Merge branch 'dev' into release-2.13 2023-11-09 19:26:08 +00:00
Laurent Cozic
b78101ef90 iOS 12.13.4 2023-11-09 19:25:36 +00:00
Laurent Cozic
6261d30574 Upgrade to Electron 26.5.0 2023-11-09 19:24:02 +00:00
Henry Heino
672d028d29 Mobile: Settings screen: Create separate pages for each screen (#8567) 2023-11-09 19:19:08 +00:00
renovate[bot]
0340c7f65c Update dependency @types/react-redux to v7.1.28 (#9242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-09 18:44:33 +00:00
pedr
632802e58b Clipper: Avoid errors when downloading media files without protocol (#9241) 2023-11-09 15:02:52 +00:00
Laurent Cozic
c3510bf26b Desktop: Fixes #9149: Toolbar icons in view mode are partly not grayed out and can be used 2023-11-09 14:58:38 +00:00
Laurent Cozic
e22aa4f6e9 Desktop: Fixes #8961: Fix rare crash when developing a plugin 2023-11-09 12:26:06 +00:00
renovate[bot]
76a8ae3a83 Update dependency @types/node-rsa to v1.1.3 (#9238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-09 11:09:16 +00:00
renovate[bot]
1c104d2e83 Update dependency @types/uuid to v9.0.6 (#9245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 23:34:41 +00:00
renovate[bot]
7c38c2c8f2 Update dependency @types/nodemailer to v6.4.13 (#9239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 22:21:00 +00:00
renovate[bot]
188d9ac159 Update dependency @types/styled-components to v5.1.29 (#9243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 20:32:10 +00:00
github-actions[bot]
91751d5fa3 @mimeyn has signed the CLA in laurent22/joplin#9244 2023-11-08 19:40:10 +00:00
renovate[bot]
5124cbfd9b Update dependency @types/node-fetch to v2.6.7 (#9237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 19:08:06 +00:00
renovate[bot]
cb21a61d7c Update dependency @types/node to v18.18.6 (#9233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 18:33:00 +00:00
Marco Rombach
f50019d098 Server: Added LDAP authentication (#9150) 2023-11-08 15:38:01 +00:00
renovate[bot]
ab9a1776c8 Update dependency @types/markdown-it to v13.0.4 (#9231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 04:43:20 +00:00
renovate[bot]
a357665c77 Update dependency @types/fs-extra to v11.0.3 (#9236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 00:36:05 +00:00
renovate[bot]
be097afd83 Update dependency @types/koa to v2.13.10 (#9230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 21:42:13 +00:00
renovate[bot]
b417299616 Update dependency @types/mustache to v4.2.4 (#9232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 19:17:44 +00:00
renovate[bot]
582b963570 Update dependency @types/jsdom to v21.1.4 (#9229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 17:06:59 +00:00
Henry Heino
1405def25d Desktop: Fixes #7547: Fix inserting resources into TinyMCE from plugins (insertText command) (#9225) 2023-11-07 12:07:42 +00:00
Henry Heino
e9c598cf46 Chore: Mobile: Remove duplicate bundle minification (#9221) 2023-11-07 12:04:33 +00:00
Henry Heino
02361e37f0 Desktop: Fixes #9036: Fix note list scroll (#9211)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-07 12:04:03 +00:00
gitbreaker222
041414b11e Doc: Fixes broken faq links (#9220) 2023-11-07 12:01:00 +00:00
Henry Heino
b4ca00ebf5 Mobile: Fixes #9207: Fix search highlighting (#9206) 2023-11-07 12:00:36 +00:00
Henry Heino
9d96866531 Mobile, Desktop: Fixes #9201: Disable selection match highlighting (#9202) 2023-11-07 12:00:13 +00:00
Henry Heino
6593025051 Desktop: Fixes #8978: Rich text editor: Fix repeated newline characters discarded on save to markdown (#9199) 2023-11-07 11:59:35 +00:00
Henry Heino
a38fe11bbe Desktop: Fixes #9122: Fix underscores escaped within some text-only URLs (#9198) 2023-11-07 11:58:52 +00:00
Roman Orlowski
b030ca914d Desktop: Fixes #9130: Allow Electron --disable-gpu flag (#9179) 2023-11-07 11:58:39 +00:00
Henry Heino
88b44a0f74 All: Fixes #8561: Fix OneDrive sync crash on throttle (#9143) 2023-11-07 11:55:38 +00:00
renovate[bot]
7b2cf0e483 Update dependency @types/js-yaml to v4.0.8 (#9227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 11:43:28 +00:00
renovate[bot]
5d3e920370 Update dependency @types/formidable to v3.4.4 (#9226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 06:07:13 +00:00
github-actions[bot]
7cec62fc71 @gitbreaker222 has signed the CLA in laurent22/joplin#9220 2023-11-06 14:32:53 +00:00
renovate[bot]
698d16e970 Update dependency @types/node to v18.18.5 (#9218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-05 21:58:35 +00:00
renovate[bot]
d88af474d2 Update dependency @types/node to v18.18.0 (#9214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-05 19:35:06 +00:00
Laurent Cozic
7488129517 Doc: Fixes #9216: Fixed several broken links 2023-11-05 17:59:36 +00:00
Laurent Cozic
9772389fd9 Doc: Fixed favicon on translated websites 2023-11-05 17:59:35 +00:00
Laurent Cozic
c2a1ea8cba Tools: Add plugin API doc to linkchecker 2023-11-05 17:59:33 +00:00
Laurent Cozic
9afc94fb3b Tools: Pin Python version to 3.11 to fix desktop build on CI (#9212) 2023-11-05 13:33:38 +00:00
github-actions[bot]
390b28c3f5 @brechsteiner has signed the CLA in laurent22/joplin#9210 2023-11-03 22:08:52 +00:00
Laurent Cozic
8be22ed910 Plugins: Add support for getting plugin settings from a Markdown renderer 2023-11-03 19:45:21 +00:00
Laurent Cozic
b097ab29ee Desktop, Mobile: Resolves #9158: Add a "Retry all" button when multiple resources could not be downloaded 2023-11-03 16:01:51 +00:00
Laurent Cozic
d9bf0b7d82 Doc: Add image source 2023-11-03 16:01:50 +00:00
renovate[bot]
0f5533af55 Update dependency electron to v26 (#9203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-03 12:40:32 +00:00
Laurent Cozic
1c47db70a0 Doc: Automatically detect Apple silicon on Download page 2023-11-03 12:36:43 +00:00
renovate[bot]
9723ab0ba6 Update dependency lint-staged to v14.0.1 (#9192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 14:36:56 +00:00
Laurent Cozic
2b9d519f4b Tools: Do not run "pod install" by default 2023-11-02 10:23:00 +00:00
renovate[bot]
4478ce118a Update dependency lint-staged to v14 (#9189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 09:54:05 +00:00
Henry Heino
7b56311729 Mobile: Fixes #9188: Image editor resets on theme change (#9190) 2023-11-02 09:53:38 +00:00
Henry Heino
09b52237f2 Mobile: Fixes #9159: Fix fast search (#9191) 2023-11-02 09:50:38 +00:00
renovate[bot]
c7c86c2b52 Update dependency deprecated-react-native-prop-types to v4.2.3 (#9186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 18:24:56 +00:00
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
Laurent Cozic
e7e5a316f1 iOS 12.13.4 2023-10-31 20:39:04 +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
403 changed files with 15844 additions and 5508 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
@@ -259,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
packages/app-desktop/gui/NoteEditor/styles/index.js
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
@@ -414,6 +416,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
@@ -423,6 +427,7 @@ packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
@@ -463,17 +468,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
@@ -596,6 +613,7 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
@@ -660,6 +678,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
@@ -821,6 +840,7 @@ packages/lib/services/rest/routes/events.test.js
packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
@@ -900,6 +920,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
@@ -975,6 +996,7 @@ 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
@@ -1013,12 +1035,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

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

@@ -33,6 +33,11 @@ jobs:
# https://yarnpkg.com/getting-started/install
corepack enable
# See github-action-main.yml for explanation
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Build macOS M1 app
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}

View File

@@ -98,6 +98,15 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# macos-latest ships with Python 3.12 by default, but this removes a
# utility that's used by electron-builder (distutils) so we need to pin
# Python to an earlier version.
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
# Ref: https://github.com/nodejs/node-gyp/issues/2869
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Run tests, build and publish Linux and macOS apps
if: runner.os == 'Linux' || runner.os == 'macOs'
env:

29
.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
@@ -241,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
packages/app-desktop/gui/NoteEditor/styles/index.js
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
@@ -396,6 +398,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
@@ -405,6 +409,7 @@ packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
@@ -445,17 +450,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
@@ -578,6 +595,7 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
@@ -642,6 +660,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
@@ -803,6 +822,7 @@ packages/lib/services/rest/routes/events.test.js
packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
@@ -882,6 +902,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
@@ -957,6 +978,7 @@ 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
@@ -995,12 +1017,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/

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:

BIN
Assets/BadgeMacOSM1.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,6 +1,31 @@
function getOs() {
async function getOs() {
// The macOS release is available for Intel and Apple silicon processors,
// and the only way to get that info is through this new
// `getHighEntropyValues` function (which is not available on all browsers).
// So here we either return "macOs" for Intel or "macOsM1" for Apple
// Silicon. If we don't know which it is, we return "macOsUndefined".
// https://stackoverflow.com/a/75177111/561309
if (navigator.appVersion.indexOf("Mac")!=-1) {
let platformInfo = null;
try {
platformInfo = await navigator.userAgentData.getHighEntropyValues(['architecture'])
} catch (error) {
console.warn('Failed getting Mac architecture:', error);
return 'macOsUndefined';
}
console.info('Got platform info:', platformInfo);
if (platformInfo.architecture === 'arm') {
return "macOsM1";
} else {
return "macOs";
}
}
if (navigator.appVersion.indexOf("Win")!=-1) return "windows";
if (navigator.appVersion.indexOf("Mac")!=-1) return "macOs";
if (navigator.appVersion.indexOf("X11")!=-1) return "linux";
if (navigator.appVersion.indexOf("Linux")!=-1) return "linux";
return null;
@@ -45,7 +70,7 @@ function setupMobileMenu() {
});
}
function setupDownloadPage() {
async function setupDownloadPage() {
if (!$('.page-download').length) return;
const downloadLinks = {};
@@ -55,6 +80,7 @@ function setupDownloadPage() {
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
if (href.endsWith('arm64.DMG')) downloadLinks['macOsM1'] = href;
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
});
@@ -70,14 +96,23 @@ function setupDownloadPage() {
if (mobileOs) {
$('.page-download .intro').hide();
} else {
const os = getOs();
if (!os || !downloadLinks[os]) {
const os = await getOs();
if (os === 'macOsUndefined') {
// If we don't know which macOS version it is, we let the user choose.
$('.main-content .intro').html('<p class="macos-m1-info">The macOS release is available for Intel processors or for Apple Silicon (M1) processors. Please select your version:</p>');
const macOsLink = $('.download-link-macOs');
const macOsM1Link = $('.download-link-macOsM1');
$('.macos-m1-info').after('<p style="font-style: italic; font-size: .8em;">To find out what processor you have, click on the <b>Apple logo</b> in the macOS menu bar, choose <b>About This Mac</b> from the dropdown menu. If you have an Apple silicon it should say"Apple M1" under "Chip". Otherwise you have an Intel processor.</p>');
$('.macos-m1-info').after(macOsM1Link);
$('.macos-m1-info').after(macOsLink);
} else if (!os || !downloadLinks[os]) {
// If we don't know, display the section to manually download the app
$('.page-download .get-it-desktop').show();
} 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];
@@ -89,5 +124,5 @@ function setupDownloadPage() {
$(function () {
setupMobileMenu();
setupDownloadPage();
void setupDownloadPage();
});

View File

@@ -19,33 +19,33 @@
<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[<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>
]]></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_to_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>
@@ -71,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>
@@ -100,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>
@@ -113,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>
@@ -131,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>
@@ -150,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>
@@ -181,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>
@@ -211,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">
@@ -277,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>
@@ -303,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>
@@ -336,8 +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></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

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

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

View File

@@ -124,7 +124,7 @@ else
fi
if [[ $LIBFUSE == "" ]] ; then
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
print "See https://joplinapp.org/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
print "See https://joplinapp.org/help/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
exit 1
fi

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.19/Joplin-Setup-2.12.19.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.19/Joplin-2.12.19.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.19/Joplin-2.12.19.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.19/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) | 84%
<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 | 21%
<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) | 54%
<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) | | 42%
<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) | 92%
<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) | 97%
<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 | 94%
<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 | 97%
<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) | 97%
<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) | | 42%
<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) | 93%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 24%
<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 | 94%
<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) | 27%
<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) | 84%
<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) | 76%
<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) | 73%
<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) | | 74%
<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) | 97%
<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) | 83%
<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) | 52%
<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) | 85%
<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) | [Fernando Nagase](mailto:nagase.fernando@gmail.com) | 97%
<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) | 68%
<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) | 48%
<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) | 76%
<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) | | 34%
<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) | | 73%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 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) | 95%
<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) | 94%
<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) | [Титан](mailto:fignin@ya.ru) | 96%
<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) | | 61%
<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) | 97%
<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) | 84%
<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) | 97%
<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) | 84%
<!-- 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",
"linkChecker": "linkchecker https://joplinapp.org/ && linkchecker --check-extern https://joplinapp.org/api/references/plugin_api/classes/joplin.html",
"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,15 +69,16 @@
}
},
"devDependencies": {
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"cspell": "5.21.2",
"eslint": "8.47.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",
@@ -83,23 +87,23 @@
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "13.3.0",
"lint-staged": "14.0.1",
"madge": "6.1.0",
"npm-package-json-lint": "7.0.0",
"typescript": "5.1.6"
"typescript": "5.2.2"
},
"dependencies": {
"@types/fs-extra": "11.0.2",
"eslint-plugin-github": "4.9.2",
"@types/fs-extra": "11.0.3",
"eslint-plugin-github": "4.10.0",
"http-server": "14.1.1",
"node-gyp": "9.4.0",
"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@8.47.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

@@ -57,6 +57,10 @@ class Command extends BaseCommand {
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.');
@@ -75,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('');
@@ -108,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('');
@@ -134,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('');
@@ -164,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('');
@@ -192,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('');
@@ -240,7 +244,7 @@ async function fetchAllNotes() {
// });
}
lines.push(`# ${toTitleCase(tableName)}`);
lines.push(`## ${toTitleCase(tableName)}`);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
@@ -248,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('');
@@ -263,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('');
@@ -314,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('');
@@ -343,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('');
@@ -356,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('');
@@ -384,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('');
@@ -400,15 +404,15 @@ 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('');
@@ -416,7 +420,7 @@ async function fetchAllNotes() {
lines.push('');
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

@@ -63,7 +63,7 @@
"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.2",
"@types/jest": "29.5.4",
"@types/node": "18.17.19",
"@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5",
"@types/node": "18.18.6",
"@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

@@ -83,8 +83,8 @@ describe('HtmlToMd', () => {
it('should allow disabling escape', async () => {
const htmlToMd = new HtmlToMd();
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: true })).toBe('> 1 _2_ 3.pdf');
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: false })).toBe('\\> 1 \\_2_ 3.pdf');
});
});

View File

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

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

@@ -15,7 +15,4 @@ however.
Because it isn't
necessary.
...
<br/><br/><br/>...

View File

@@ -0,0 +1,4 @@
A<br/><br/><br/>test.<br/>
A single &lt;br/&gt;<br/>can use two spaces at the end of the line,
but<br/><br/>the markdown renderer discards these if the line is otherwise empty.

View File

@@ -0,0 +1,5 @@
A
<br/><br/>test.
A single &lt;br/&gt;
can use two spaces at the end of the line, but
<br/>the markdown renderer discards these if the line is otherwise empty.

View File

@@ -0,0 +1,14 @@
<p>
Some URLs in the Rich_Text_Editor contain <code>_</code> characters, but haven't been converted
to links yet. For example, https://www.example.com/a_test_of_links.
</p>
<p>We should preserve the underscores _without escaping them_ to prevent the links from breaking.</p>
<p>
This should also correctly handle unicode characters. For example, punctuation❯_requires escapes_,
but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
</p>
<p>
_Note_ that what [_causes_] a `_` to create italics_ seems to depend only on the character before
and an escape at the _beginning_ seems to be sufficient.
</p>
<p>_s also don't need escapes if _ followed _ by a _space.</p>

View File

@@ -0,0 +1,9 @@
Some URLs in the Rich_Text_Editor contain `_` characters, but haven't been converted to links yet. For example, https://www.example.com/a_test_of_links.
We should preserve the underscores \_without escaping them_ to prevent the links from breaking.
This should also correctly handle unicode characters. For example, punctuation❯\_requires escapes_, but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
\_Note_ that what \[\_causes_\] a \`\_\` to create italics_ seems to depend only on the character before and an escape at the \_beginning_ seems to be sufficient.
\_s also don't need escapes if _ followed _ by a \_space.

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

@@ -206,7 +206,7 @@ describe('services_PluginService', () => {
const mdToHtml = new MdToHtml();
const module = require(contentScript.path).default;
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}));
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
const result = await mdToHtml.render([
'```justtesting',

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

@@ -74,7 +74,7 @@ To get such an external script file to compile, you need to add it to the `extra
## More information
- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
- [Joplin Data API](https://joplinapp.org/api/references/rest_api/)
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)

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

View File

@@ -9,7 +9,10 @@ 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;
@@ -34,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;
@@ -66,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_;
@@ -121,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:',

View File

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

@@ -12,7 +12,7 @@ const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
import ClipperConfigScreen from '../ClipperConfigScreen';
import restart from '../../services/restart';
import PluginService from '@joplin/lib/services/plugins/PluginService';
@@ -35,9 +35,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
public constructor(props: any) {
super(props);
shared.init(this, reg);
shared.init(reg);
this.state = {
...shared.defaultScreenState,
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
@@ -98,7 +99,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
public sectionByName(name: string) {
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings });
for (const section of sections) {
if (section.name === name) return section;
}
@@ -699,7 +700,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const hasChanges = this.hasChanges();
const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName);
const settingComps = shared.settingsToComponents2(this, AppType.Desktop, settings, this.state.selectedSectionName);
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
// These screens handle their own loading/saving of settings and have bespoke rendering.
@@ -708,7 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
if (screenComp) containerStyle.display = 'none';
const sections = shared.settingsSections({ device: 'desktop', settings });
const sections = shared.settingsSections({ device: AppType.Desktop, settings });
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>

View File

@@ -1,4 +1,4 @@
import { SettingSectionSource } from '@joplin/lib/models/Setting';
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
import * as React from 'react';
import { useMemo } from 'react';
import Setting from '@joplin/lib/models/Setting';
@@ -93,7 +93,9 @@ export default function Sidebar(props: Props) {
const selected = props.selection === section.name;
return (
<StyledListItem key={section.name} isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
<StyledListItemIcon
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
/>
<StyledListItemLabel>
{Setting.sectionNameToLabel(section.name)}
</StyledListItemLabel>

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

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

@@ -136,7 +136,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
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);
@@ -214,7 +218,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
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: () => {
@@ -255,7 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
},
};
// 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);

View File

@@ -151,6 +151,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorCopyText, editorCutText, editorPaste,
editorContent: props.content,
visiblePanes: props.visiblePanes,
contentMarkupLanguage: props.contentMarkupLanguage,
});
useImperativeHandle(ref, () => {

View File

@@ -7,6 +7,7 @@ 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');
@@ -40,6 +41,7 @@ interface Props {
selectionRange: { from: number; to: number };
visiblePanes: string[];
contentMarkupLanguage: MarkupLanguage;
}
const useEditorCommands = (props: Props) => {
@@ -57,7 +59,7 @@ const useEditorCommands = (props: Props) => {
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 });
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);
@@ -92,7 +94,7 @@ const useEditorCommands = (props: Props) => {
insertText: (value: any) => editorRef.current.insertText(value),
attachFile: async () => {
const newBody = await commandAttachFileToBody(
props.editorContent, null, { position: props.selectionRange.from },
props.editorContent, null, { position: props.selectionRange.from, markupLanguage: props.contentMarkupLanguage },
);
if (newBody) {
editorRef.current.updateBody(newBody);
@@ -129,7 +131,7 @@ const useEditorCommands = (props: Props) => {
}, [
props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste,
props.selectionRange,
props.contentMarkupLanguage,
props.webviewRef, editorRef,
]);
};

View File

@@ -203,7 +203,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
let commandProcessed = true;
if (cmd.name === 'insertText') {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, { bodyOnly: true });
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);
} else if (cmd.name === 'editor.focus') {
editor.focus();

View File

@@ -48,6 +48,7 @@ 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';
import { namespacedKey } from '@joplin/lib/services/plugins/api/JoplinSettings';
const commands = [
require('./commands/showRevisions'),
@@ -159,10 +160,15 @@ function NoteEditor(props: NoteEditorProps) {
return formNote.saveActionQueue.waitForAllDone();
}
const settingValue = useCallback((pluginId: string, key: string) => {
return Setting.value(namespacedKey(pluginId, key));
}, []);
const markupToHtml = useMarkupToHtml({
themeId: props.themeId,
customCss: props.customCss,
plugins: props.plugins,
settingValue,
});
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
@@ -473,7 +479,7 @@ function NoteEditor(props: NoteEditorProps) {
}
const onRichTextReadMoreLinkClick = useCallback(() => {
bridge().openExternal('https://joplinapp.org/rich_text_editor');
bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
}, []);
const onRichTextDismissLinkClick = useCallback(() => {

View File

@@ -0,0 +1,64 @@
import WhenClause from '@joplin/lib/services/WhenClause';
import { enabledCondition } from './editorCommandDeclarations';
const baseContext: Record<string, any> = {
modalDialogVisible: false,
gotoAnythingVisible: false,
markdownEditorPaneVisible: true,
oneNoteSelected: true,
noteIsMarkdown: true,
noteIsReadOnly: false,
richTextEditorVisible: false,
};
describe('editorCommandDeclarations', () => {
test.each([
[
{},
true,
],
[
{
markdownEditorPaneVisible: false,
},
false,
],
[
{
noteIsReadOnly: true,
},
false,
],
[
// In the Markdown editor, but only the viewer is visible
{
markdownEditorPaneVisible: false,
richTextEditorVisible: false,
},
false,
],
[
// In the Markdown editor, and the viewer is visible
{
markdownEditorPaneVisible: true,
richTextEditorVisible: false,
},
true,
],
[
// In the RT editor
{
markdownEditorPaneVisible: false,
richTextEditorVisible: true,
},
true,
],
])('should create the enabledCondition', (context: Record<string, any>, expected: boolean) => {
const condition = enabledCondition('textBold');
const wc = new WhenClause(condition);
const actual = wc.evaluate({ ...baseContext, ...context });
expect(actual).toBe(expected);
});
});

View File

@@ -2,9 +2,24 @@ 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);
const output = [
'!modalDialogVisible',
'!gotoAnythingVisible',
markdownEditorOnly ? 'markdownEditorPaneVisible' : '(markdownEditorPaneVisible || richTextEditorVisible)',
'oneNoteSelected',
noteMustBeMarkdown ? 'noteIsMarkdown' : '',
'!noteIsReadOnly',
];
return output.filter(c => !!c).join(' && ');
};
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

@@ -12,6 +12,7 @@ interface HookDependencies {
themeId: number;
customCss: string;
plugins: PluginStates;
settingValue: (pluginId: string, key: string)=> any;
}
export interface MarkupToHtmlOptions {
@@ -59,12 +60,16 @@ export default function useMarkupToHtml(deps: HookDependencies) {
delete options.replaceResourceInternalToExternalLinks;
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss,
const result = await markupToHtml.render(markupLanguage, md, theme, {
codeTheme: theme.codeThemeCss,
resources: resources,
postMessageSyntax: 'ipcProxySendToHost',
splitted: true,
externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml', ...options });
codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
...options,
});
return result;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied

View File

@@ -145,6 +145,7 @@ const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
tabIndex={0}
className={className}
data-id={noteId}
style={{ height: props.itemSize.height }}
onContextMenu={props.onContextMenu}
onDragStart={props.onDragStart}
onDragOver={props.onDragOver}

View File

@@ -2,18 +2,18 @@ import * as React from 'react';
import { useState, useEffect } from 'react';
import ButtonBar from '../ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
const { themeStyle } = require('@joplin/lib/theme');
import ReportService from '@joplin/lib/services/ReportService';
import { themeStyle } from '@joplin/lib/theme';
import ReportService, { ReportItem, ReportSection, RetryAllHandler } from '@joplin/lib/services/ReportService';
import Button, { ButtonLevel } from '../Button/Button';
import bridge from '../../services/bridge';
const fs = require('fs-extra');
import styled from 'styled-components';
import { AppState } from '../../app.reducer';
import { writeFileSync } from 'fs';
interface Props {
themeId: string;
themeId: number;
style: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
dispatch: Function;
@@ -34,12 +34,12 @@ async function exportDebugReportClick() {
if (!filePath) return;
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
await fs.writeFileSync(filePath, csv);
const csv = (await service.basicItemList({ format: 'csv' })) as string;
await writeFileSync(filePath, csv);
}
function StatusScreen(props: Props) {
const [report, setReport] = useState<any[]>([]);
const [report, setReport] = useState<ReportSection[]>([]);
async function resfreshScreen() {
const service = new ReportService();
@@ -65,7 +65,7 @@ function StatusScreen(props: Props) {
const containerStyle = { ...theme.containerStyle, padding: containerPadding,
flex: 1 };
function renderSectionTitleHtml(key: string, title: string) {
function renderSectionTitle(key: string, title: string) {
return (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
@@ -73,7 +73,7 @@ function StatusScreen(props: Props) {
);
}
function renderSectionRetryAllHtml(key: string, retryAllHandler: any) {
function renderSectionRetryAll(key: string, retryAllHandler: RetryAllHandler) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
@@ -81,13 +81,26 @@ function StatusScreen(props: Props) {
);
}
const renderSectionHtml = (key: string, section: any) => {
const itemsHtml = [];
const renderRetryAll = (section: ReportSection) => {
const items: React.JSX.Element[] = [];
if (section.canRetryAll) {
items.push(renderSectionRetryAll(section.title, async () => {
await section.retryAllHandler();
void resfreshScreen();
}));
}
return items;
};
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
const renderSection = (key: string, section: ReportSection) => {
let items = [];
items.push(renderSectionTitle(section.title, section.title));
items = items.concat(renderRetryAll(section));
let currentListKey = '';
let listItems: any[] = [];
let listItems: React.JSX.Element[] = [];
for (const n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
const item = section.body[n];
@@ -115,12 +128,12 @@ function StatusScreen(props: Props) {
}
if (itemType === 'openList') {
currentListKey = item.key;
currentListKey = (item as ReportItem).key;
continue;
}
if (itemType === 'closeList') {
itemsHtml.push(<ul key={currentListKey}>{listItems}</ul>);
items.push(<ul key={currentListKey}>{listItems}</ul>);
currentListKey = '';
listItems = [];
continue;
@@ -136,7 +149,7 @@ function StatusScreen(props: Props) {
</li>,
);
} else {
itemsHtml.push(
items.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
@@ -145,26 +158,21 @@ function StatusScreen(props: Props) {
}
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, async () => {
await section.retryAllHandler();
void resfreshScreen();
}));
}
items = items.concat(renderRetryAll(section));
return <div key={key}>{itemsHtml}</div>;
return <div key={key}>{items}</div>;
};
function renderBodyHtml(report: any) {
const sectionsHtml = [];
function renderBody(report: ReportSection[]) {
const sections = [];
for (let i = 0; i < report.length; i++) {
const section = report[i];
if (!section.body.length) continue;
sectionsHtml.push(renderSectionHtml(`${i}`, section));
sections.push(renderSection(`${i}`, section));
}
return <div>{sectionsHtml}</div>;
return <div>{sections}</div>;
}
function renderTools() {
@@ -180,7 +188,7 @@ function StatusScreen(props: Props) {
);
}
const body = renderBodyHtml(report);
const body = renderBody(report);
return (
<div style={style}>
@@ -195,7 +203,7 @@ function StatusScreen(props: Props) {
);
}
const mapStateToProps = (state: any) => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
settings: state.settings,

View File

@@ -2,6 +2,9 @@ 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', () => {
@@ -121,4 +124,23 @@ test.describe('main', () => {
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

@@ -6,6 +6,7 @@ import uuid from '@joplin/lib/uuid';
type JoplinFixtures = {
profileDirectory: string;
electronApp: ElectronApplication;
mainWindow: Page;
};
@@ -20,19 +21,26 @@ export const test = base.extend<JoplinFixtures>({
// See https://github.com/microsoft/playwright/issues/8798
//
// eslint-disable-next-line no-empty-pattern
electronApp: async ({ }, use) => {
profileDirectory: async ({ }, use) => {
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
const profileSubdir = join(profilePath, uuid.createNano());
await mkdirp(profileSubdir);
const startupArgs = ['main.js', '--env', 'dev', '--profile', 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();
await remove(profileSubdir);
},
mainWindow: async ({ electronApp }, use) => {

View File

@@ -31,117 +31,122 @@ const React = require('react');
const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default;
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.
@@ -150,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

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.3",
"version": "2.13.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -119,21 +119,21 @@
"@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.19",
"@types/jest": "29.5.5",
"@types/node": "18.18.6",
"@types/react": "18.2.31",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"electron": "25.9.0",
"electron-builder": "24.4.0",
"@types/react-redux": "7.1.28",
"@types/styled-components": "5.1.29",
"electron": "26.5.0",
"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.18.0",
"react-test-renderer": "18.2.0",
"typescript": "5.1.6"
"typescript": "5.2.2"
},
"optionalDependencies": {
"7zip-bin-linux": "^1.0.1",
@@ -142,14 +142,14 @@
},
"dependencies": {
"@electron/notarize": "2.1.0",
"@electron/remote": "2.0.11",
"@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.3",
"@types/mustache": "4.2.4",
"async-mutex": "0.4.0",
"codemirror": "5.65.9",
"color": "3.2.1",

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

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

@@ -32,7 +32,7 @@ export default function(frameWindow: any, isReady: boolean, postMessage: Functio
frameWindow.addEventListener('message', onMessage);
return () => {
frameWindow.removeEventListener('message', onMessage);
if (frameWindow.removeEventListener) frameWindow.removeEventListener('message', onMessage);
};
}, [frameWindow, htmlHash]);

View File

@@ -5,8 +5,8 @@
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
TEMP_PATH=~/src/plugin-tests
NEED_COMPILING=0
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple
NEED_COMPILING=1
PLUGIN_PATH=~/src/plugin-abc
if [[ $NEED_COMPILING == 1 ]]; then
mkdir -p "$TEMP_PATH"

View File

@@ -0,0 +1,45 @@
let currentProfileDirectory: string;
jest.doMock('../bridge', () => ({
// Mock the bridge functions used by restartInSafeModeFromMain
// to remove the dependency on Electron.
default: () => ({
restart: jest.fn(),
processArgv: () => [
// The argument parser expects the first two arguments to
// be the path to NodeJS and the second to be the main filename.
process.argv[0], __filename,
// Only the following arguments are used.
'--profile', currentProfileDirectory,
],
env: () => 'dev',
}),
}));
import { mkdtemp, readFile, remove } from 'fs-extra';
import restartInSafeModeFromMain from './restartInSafeModeFromMain';
import { tmpdir } from 'os';
import { join } from 'path';
import { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
describe('restartInSafeModeFromMain', () => {
beforeEach(async () => {
currentProfileDirectory = await mkdtemp(join(tmpdir(), 'safemode-restart-test'));
});
afterEach(async () => {
await remove(currentProfileDirectory);
});
test('should create a safe mode flag file', async () => {
await restartInSafeModeFromMain();
const safeModeFlagFilepath = join(
currentProfileDirectory, safeModeFlagFilename,
);
expect(await readFile(safeModeFlagFilepath, 'utf8')).toBe('true');
});
});

View File

@@ -0,0 +1,33 @@
import Setting from '@joplin/lib/models/Setting';
import bridge from '../bridge';
import processStartFlags from '@joplin/lib/utils/processStartFlags';
import BaseApplication, { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
import { writeFile } from 'fs-extra';
import { join } from 'path';
const restartInSafeModeFromMain = async () => {
// Only set constants here -- the main process doesn't have easy access (without loading
// a large amount of other code) to the database.
const appName = `joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`;
Setting.setConstant('appId', `net.cozic.${appName}`);
Setting.setConstant('appType', 'desktop');
Setting.setConstant('appName', appName);
// Load just enough for us to write a file in the profile directory
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit({});
const startFlags = await processStartFlags(bridge().processArgv());
const rootProfileDir = BaseApplication.determineProfileDir(startFlags.matched);
const { profileDir } = await initProfile(rootProfileDir);
// We can't access the database, so write to a file instead.
const safeModeFlagFile = join(profileDir, safeModeFlagFilename);
await writeFile(safeModeFlagFile, 'true', 'utf8');
bridge().restart();
};
export default restartInSafeModeFromMain;

View File

@@ -110,8 +110,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097724
versionName "2.13.4"
versionCode 2097726
versionName "2.13.6"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { TextStyle } from 'react-native';
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
interface Props {
name: string;
style: TextStyle;
// If `null` is given, the content must be labeled elsewhere.
accessibilityLabel: string|null;
}
const Icon: React.FC<Props> = props => {
// Matches:
// 1. A prefix of word characters (\w+)
// 2. A suffix of non-spaces (\S+)
// An "fa-" at the beginning of the suffix is ignored.
const nameMatch = props.name.match(/^(\w+)\s+(?:fa-)?(\S+)$/);
const namePrefix = nameMatch ? nameMatch[1] : '';
const nameSuffix = nameMatch ? nameMatch[2] : props.name;
// If there's no label, make sure that the screen reader doesn't try
// to read the characters from the icon font (they don't make sense
// without the icon font applied).
const accessibilityHidden = props.accessibilityLabel === null;
return (
<FontAwesomeIcon
brand={namePrefix.startsWith('fab')}
solid={namePrefix.startsWith('fas')}
accessibilityLabel={props.accessibilityLabel}
aria-hidden={accessibilityHidden}
importantForAccessibility={
accessibilityHidden ? 'no-hide-descendants' : 'yes'
}
name={nameSuffix}
style={props.style}
/>
);
};
export default Icon;

View File

@@ -16,11 +16,13 @@ const logger = Logger.create('ImageEditor');
type OnSaveCallback = (svgData: string)=> void;
type OnCancelCallback = ()=> void;
// Returns the empty string to load from a template.
type LoadInitialSVGCallback = ()=> Promise<string>;
interface Props {
themeId: number;
loadInitialSVGData: LoadInitialSVGCallback|null;
loadInitialSVGData: LoadInitialSVGCallback;
onSave: OnSaveCallback;
onExit: OnCancelCallback;
}
@@ -128,20 +130,31 @@ const ImageEditor = (props: Props) => {
}, [onRequestCloseEditor]);
const css = useCss(editorTheme);
const html = useMemo(() => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
const [html, setHtml] = useState('');
<style>
${css}
</style>
</head>
<body></body>
</html>
`, [css]);
useEffect(() => {
setHtml(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
<style id='main-style'>
${css}
</style>
</head>
<body></body>
</html>
`);
// Only set HTML initially (and don't reset). Changing the HTML reloads
// the page.
//
// We need the HTML to initially have the correct CSS to prevent color
// changes on load.
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps
}, []);
// A set of localization overrides (Joplin is better localized than js-draw).
// All localizable strings (some unused?) can be found at
@@ -233,11 +246,13 @@ const ImageEditor = (props: Props) => {
useEffect(() => {
webviewRef.current?.injectJS(`
document.querySelector('#main-style').innerText = ${JSON.stringify(css)};
if (window.editorControl) {
window.editorControl.onThemeUpdate();
}
`);
}, [editorTheme]);
}, [css]);
const onReadyToLoadData = useCallback(async () => {
const initialSVGData = await props.loadInitialSVGData?.() ?? '';

View File

@@ -6,8 +6,8 @@ import { defaultSearchState, SearchPanel } from './SearchPanel';
import ExtendedWebView from '../ExtendedWebView';
import * as React from 'react';
import { forwardRef, RefObject, useImperativeHandle } from 'react';
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
import { forwardRef, useImperativeHandle } from 'react';
import { useMemo, useState, useCallback, useRef } from 'react';
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
const { editorFont } = require('../global-style');
@@ -126,7 +126,6 @@ type OnSearchStateChangeCallback = (state: SearchState)=> void;
const useEditorControl = (
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
setSearchState: OnSearchStateChangeCallback,
searchStateRef: RefObject<SearchState>,
): EditorControl => {
return useMemo(() => {
const execCommand = (command: EditorCommandType) => {
@@ -252,16 +251,10 @@ const useEditorControl = (
},
showSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: true,
});
execCommand(EditorCommandType.ShowSearch);
},
hideSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: false,
});
execCommand(EditorCommandType.HideSearch);
},
setSearchState: setSearchStateCallback,
@@ -269,7 +262,7 @@ const useEditorControl = (
};
return control;
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
}, [injectJS, setLinkDialogVisible, setSearchState]);
};
function NoteEditor(props: Props, ref: any) {
@@ -356,22 +349,13 @@ function NoteEditor(props: Props, ref: any) {
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
const [searchState, setSearchState] = useState(defaultSearchState);
// Having a [searchStateRef] allows [editorControl] to not be re-created
// whenever [searchState] changes.
const searchStateRef = useRef(defaultSearchState);
// Keep the reference and the [searchState] in sync
useEffect(() => {
searchStateRef.current = searchState;
}, [searchState]);
// Runs [js] in the context of the CodeMirror frame.
const injectJS = (js: string) => {
webviewRef.current.injectJS(js);
};
const editorControl = useEditorControl(
injectJS, setLinkDialogVisible, setSearchState, searchStateRef,
injectJS, setLinkDialogVisible, setSearchState,
);
useImperativeHandle(ref, () => {

View File

@@ -1,12 +1,12 @@
const React = require('react');
const { StyleSheet } = require('react-native');
import * as React from 'react';
import { StyleSheet } from 'react-native';
const { themeStyle } = require('./global-style.js');
const rootStyles_ = {};
const rootStyles_: Record<number, any> = {};
class BaseScreenComponent extends React.Component {
class BaseScreenComponent<Props, State> extends React.Component<Props, State> {
rootStyle(themeId) {
protected rootStyle(themeId: number) {
const theme = themeStyle(themeId);
if (rootStyles_[themeId]) return rootStyles_[themeId];
rootStyles_[themeId] = StyleSheet.create({
@@ -19,4 +19,5 @@ class BaseScreenComponent extends React.Component {
}
}
module.exports = { BaseScreenComponent };
export { BaseScreenComponent };
export default BaseScreenComponent;

View File

@@ -1,214 +1,168 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import Slider from '@react-native-community/slider';
const React = require('react');
import { Platform, Linking, View, Switch, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native';
import * as React from 'react';
import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import Setting, { AppType } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import ReportService from '@joplin/lib/services/ReportService';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
import checkPermissions from '../../../utils/checkPermissions';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import setIgnoreTlsErrors from '../../../utils/TlsUtils';
import { reg } from '@joplin/lib/registry';
import { State } from '@joplin/lib/reducer';
const { BackButtonService } = require('../../../services/back-button.js');
const VersionInfo = require('react-native-version-info').default;
const { connect } = require('react-redux');
import { connect } from 'react-redux';
import ScreenHeader from '../../ScreenHeader';
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../../base-screen.js');
const { Dropdown } = require('../../Dropdown');
import { _ } from '@joplin/lib/locale';
import BaseScreenComponent from '../../base-screen';
const { themeStyle } = require('../../global-style.js');
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles from './configScreenStyles';
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton';
import ConfigScreenButton from './ConfigScreenButton';
import SettingsButton from './SettingsButton';
import Clipboard from '@react-native-community/clipboard';
import { ReactNode } from 'react';
import { Dispatch } from 'redux';
import SectionHeader from './SectionHeader';
import ExportProfileButton from './NoteExportSection/ExportProfileButton';
import SettingComponent from './SettingComponent';
import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton';
import SectionSelector from './SectionSelector';
class ConfigScreenComponent extends BaseScreenComponent {
interface ConfigScreenState {
settings: any;
changedSettingKeys: string[];
fixingSearchIndex: boolean;
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
showAdvancedSettings: boolean;
selectedSectionName: string|null;
sidebarWidth: number;
}
interface ConfigScreenProps {
settings: any;
themeId: number;
navigation: any;
dispatch: Dispatch;
}
class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, ConfigScreenState> {
public static navigationOptions(): any {
return { header: null };
}
private componentsY_: Record<string, number> = {};
private styles_: Record<number, ConfigScreenStyles> = {};
private scrollViewRef_: React.RefObject<ScrollView>;
public constructor() {
super();
this.styles_ = {};
public constructor(props: ConfigScreenProps) {
super(props);
this.state = {
creatingReport: false,
profileExportStatus: 'idle',
profileExportPath: '',
fileSystemSyncPath: Setting.value('sync.2.path'),
...shared.defaultScreenState,
selectedSectionName: null,
fixingSearchIndex: false,
sidebarWidth: 100,
};
this.scrollViewRef_ = React.createRef();
this.scrollViewRef_ = React.createRef<ScrollView>();
shared.init(this, reg);
this.selectDirectoryButtonPress = async () => {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
this.setState({ fileSystemSyncPath: doc.uri });
shared.updateSettingValue(this, 'sync.2.path', doc.uri);
} else {
throw new Error('User cancelled operation');
}
} catch (e) {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
};
this.checkSyncConfig_ = async () => {
// to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state
// this call sets the new value and returns the previous one which we can use later to revert the change
const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']);
const result = await shared.checkSyncConfig(this, this.state.settings);
if (!result || !result.ok) {
await setIgnoreTlsErrors(prevIgnoreTlsErrors);
}
};
this.e2eeConfig_ = () => {
void NavService.go('EncryptionConfig');
};
this.saveButton_press = async () => {
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
if (Platform.OS === 'android') {
if (Platform.Version < 29) {
if (!(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
}
}
}
// Save settings anyway, even if permission has not been granted
}
// changedSettingKeys is cleared in shared.saveSettings so reading it now
const setIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors');
await shared.saveSettings(this);
if (setIgnoreTlsErrors) {
await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors'));
}
};
this.saveButton_press = this.saveButton_press.bind(this);
this.syncStatusButtonPress_ = () => {
void NavService.go('Status');
};
this.manageProfilesButtonPress_ = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileSwitcher',
});
};
this.exportDebugButtonPress_ = async () => {
this.setState({ creatingReport: true });
const service = new ReportService();
const logItems = await reg.logger().lastEntries(null);
const logItemRows = [['Date', 'Level', 'Message']];
for (let i = 0; i < logItems.length; i++) {
const item = logItems[i];
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
}
const logItemCsv = service.csvCreate(logItemRows);
const itemListCsv = await service.basicItemList({ format: 'csv' });
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
this.setState({ creatingReport: false });
return;
}
const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`;
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
alert(`Debug report exported to ${filePath}`);
this.setState({ creatingReport: false });
};
this.fixSearchEngineIndexButtonPress_ = async () => {
this.setState({ fixingSearchIndex: true });
await SearchEngine.instance().rebuildIndex();
this.setState({ fixingSearchIndex: false });
};
this.exportProfileButtonPress_ = async () => {
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
return;
}
const p = this.state.profileExportPath ? this.state.profileExportPath : `${externalDir}/JoplinProfileExport`;
this.setState({
profileExportStatus: 'prompt',
profileExportPath: p,
});
};
this.exportProfileButtonPress2_ = async () => {
this.setState({ profileExportStatus: 'exporting' });
const dbPath = '/data/data/net.cozic.joplin/databases';
const exportPath = this.state.profileExportPath;
const resourcePath = `${exportPath}/resources`;
try {
const response = await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
if (response !== PermissionsAndroid.RESULTS.GRANTED) {
throw new Error('Permission denied');
}
const copyFiles = async (source: string, dest: string) => {
await shim.fsDriver().mkdir(dest);
const files = await shim.fsDriver().readDirStats(source);
for (const file of files) {
const source_ = `${source}/${file.path}`;
const dest_ = `${dest}/${file.path}`;
if (!file.isDirectory()) {
reg.logger().info(`Copying profile: ${source_} => ${dest_}`);
await shim.fsDriver().copy(source_, dest_);
} else {
await copyFiles(source_, dest_);
}
}
};
await copyFiles(dbPath, exportPath);
await copyFiles(Setting.value('resourceDir'), resourcePath);
alert('Profile has been exported!');
} catch (error) {
alert(`Could not export files: ${error.message}`);
} finally {
this.setState({ profileExportStatus: 'idle' });
}
};
this.logButtonPress_ = () => {
void NavService.go('Log');
};
this.handleSetting = this.handleSetting.bind(this);
shared.init(reg);
}
private checkSyncConfig_ = async () => {
// to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state
// this call sets the new value and returns the previous one which we can use later to revert the change
const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']);
const result = await shared.checkSyncConfig(this, this.state.settings);
if (!result || !result.ok) {
await setIgnoreTlsErrors(prevIgnoreTlsErrors);
}
};
private e2eeConfig_ = () => {
void NavService.go('EncryptionConfig');
};
private saveButton_press = async () => {
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
if (Platform.OS === 'android') {
if (Platform.Version < 29) {
if (!(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
}
}
}
// Save settings anyway, even if permission has not been granted
}
// changedSettingKeys is cleared in shared.saveSettings so reading it now
const shouldSetIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors');
await shared.saveSettings(this);
if (shouldSetIgnoreTlsErrors) {
await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors'));
}
};
private syncStatusButtonPress_ = () => {
void NavService.go('Status');
};
private manageProfilesButtonPress_ = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileSwitcher',
});
};
private fixSearchEngineIndexButtonPress_ = async () => {
this.setState({ fixingSearchIndex: true });
await SearchEngine.instance().rebuildIndex();
this.setState({ fixingSearchIndex: false });
};
private logButtonPress_ = () => {
void NavService.go('Log');
};
private updateSidebarWidth = () => {
const windowWidth = Dimensions.get('window').width;
let sidebarNewWidth = windowWidth;
const sidebarValidWidths = [280, 230];
const maxFractionOfWindowSize = 1 / 3;
for (const width of sidebarValidWidths) {
if (width < windowWidth * maxFractionOfWindowSize) {
sidebarNewWidth = width;
break;
}
}
this.setState({ sidebarWidth: sidebarNewWidth });
};
private navigationFillsScreen() {
const windowWidth = Dimensions.get('window').width;
return this.state.sidebarWidth > windowWidth / 2;
}
private switchSectionPress_ = (section: string) => {
const label = Setting.sectionNameToLabel(section);
AccessibilityInfo.announceForAccessibility(_('Opening section %s', label));
this.setState({ selectedSectionName: section });
};
private showSectionNavigation_ = () => {
this.setState({ selectedSectionName: null });
};
public async checkFilesystemPermission() {
if (Platform.OS !== 'android') {
// Not implemented yet
@@ -225,7 +179,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
this.setState({ settings: this.props.settings });
}
public styles() {
public styles(): ConfigScreenStyles {
const themeId = this.props.themeId;
if (this.styles_[themeId]) return this.styles_[themeId];
@@ -258,6 +212,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
await BackButtonService.back();
};
// Show navigation when pressing "back" (unless always visible).
if (this.state.selectedSectionName && this.navigationFillsScreen()) {
this.showSectionNavigation_();
return true;
}
if (this.state.changedSettingKeys.length > 0) {
const dialogTitle: string|null = null;
Alert.alert(
@@ -284,6 +244,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
public componentDidMount() {
if (this.props.navigation.state.sectionName) {
this.setState({ selectedSectionName: this.props.navigation.state.sectionName });
setTimeout(() => {
this.scrollViewRef_.current.scrollTo({
x: 0,
@@ -294,24 +255,17 @@ class ConfigScreenComponent extends BaseScreenComponent {
}
BackButtonService.addHandler(this.handleBackButtonPress);
Dimensions.addEventListener('change', this.updateSidebarWidth);
this.updateSidebarWidth();
}
public componentWillUnmount() {
BackButtonService.removeHandler(this.handleBackButtonPress);
}
public renderHeader(key: string, title: string) {
const theme = themeStyle(this.props.themeId);
return (
<View key={key} style={this.styles().headerWrapperStyle} onLayout={(event: any) => this.onHeaderLayout(key, event)}>
<Text style={theme.headerStyle}>{title}</Text>
</View>
);
}
private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
return (
<ConfigScreenButton
<SettingsButton
key={key}
title={title}
clickHandler={clickHandler}
@@ -322,9 +276,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
);
}
public sectionToComponent(key: string, section: any, settings: any) {
public sectionToComponent(key: string, section: any, settings: any, isSelected: boolean) {
const settingComps = [];
const styleSheet = this.styles().styleSheet;
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
@@ -335,10 +291,10 @@ class ConfigScreenComponent extends BaseScreenComponent {
const messages = shared.checkSyncConfigMessages(this);
const statusComp = !messages.length ? null : (
<View style={{ flex: 1, marginTop: 10 }}>
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
<Text style={this.styles().styleSheet.descriptionText}>{messages[0]}</Text>
{messages.length >= 1 ? (
<View style={{ marginTop: 10 }}>
<Text style={this.styles().descriptionText}>{messages[1]}</Text>
<Text style={this.styles().styleSheet.descriptionText}>{messages[1]}</Text>
</View>
) : null}
</View>
@@ -360,8 +316,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook');
settingComps.push(
<View key="joplinCloud">
<View style={this.styles().settingContainerNoBottomBorder}>
<Text style={this.styles().settingText}>{_('Email to note')}</Text>
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
<Text style={this.styles().styleSheet.settingText}>{_('Email to note')}</Text>
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
</View>
{
@@ -376,11 +332,129 @@ class ConfigScreenComponent extends BaseScreenComponent {
);
}
if (section.name === 'tools') {
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') }));
}
if (section.name === 'export') {
settingComps.push(<NoteExportButton key='export_as_jex_button' styles={this.styles()} />);
settingComps.push(<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>);
settingComps.push(<ExportProfileButton key='export_data' styles={this.styles()}/>);
}
if (section.name === 'moreInfo') {
if (Platform.OS === 'android' && Platform.Version >= 23) {
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
settingComps.push(
<View key="permission_info" style={styleSheet.settingContainer}>
<View key="permission_info_wrapper">
<Text key="perm1a" style={styleSheet.settingText}>
{_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')}
</Text>
<Text key="perm2" style={styleSheet.permissionText}>
{_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')}
</Text>
<Text key="perm3" style={styleSheet.permissionText}>
{_('- Camera: to allow taking a picture and attaching it to a note.')}
</Text>
<Text key="perm4" style={styleSheet.permissionText}>
{_('- Location: to allow attaching geo-location information to a note.')}
</Text>
</View>
</View>,
);
}
settingComps.push(
<View key="donate_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Make a donation')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="website_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Joplin website')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="privacy_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/privacy/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Privacy Policy')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="version_info_app" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
</View>,
);
settingComps.push(
<View key="version_info_db" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Database v%s', reg.db().version())}</Text>
</View>,
);
settingComps.push(
<View key="version_info_fts" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
</View>,
);
settingComps.push(
<View key="version_info_hermes" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
</View>,
);
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) {
const headerKey = 'featureFlags';
settingComps.push(<SectionHeader
key={headerKey}
styles={this.styles().styleSheet}
title={_('Feature flags')}
onLayout={event => this.onHeaderLayout(headerKey, event)}
/>);
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>);
}
}
if (!settingComps.length) return null;
if (!isSelected) return null;
return (
<View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}>
{this.renderHeader(section.name, Setting.sectionNameToLabel(section.name))}
<View>{settingComps}</View>
</View>
);
@@ -392,22 +466,18 @@ class ConfigScreenComponent extends BaseScreenComponent {
return (
<View key={key}>
<View style={this.containerStyle(false)}>
<Text key="label" style={this.styles().switchSettingText}>
<View style={this.styles().getContainerStyle(false)}>
<Text key="label" style={this.styles().styleSheet.switchSettingText}>
{label}
</Text>
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
<Switch key="control" style={this.styles().styleSheet.switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
</View>
{descriptionComp}
</View>
);
}
private containerStyle(hasDescription: boolean): any {
return !hasDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
}
private async handleSetting(key: string, value: any): Promise<boolean> {
private handleSetting = async (key: string, value: any): Promise<boolean> => {
// When the user tries to enable biometrics unlock, we ask for the
// fingerprint or Face ID, and if it's correct we save immediately. If
// it's not, we don't turn on the setting.
@@ -428,123 +498,24 @@ class ConfigScreenComponent extends BaseScreenComponent {
}
return false;
}
};
public settingToComponent(key: string, value: any) {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
const output: any = null;
const updateSettingValue = async (key: string, value: any) => {
const handled = await this.handleSetting(key, value);
if (!handled) shared.updateSettingValue(this, key, value);
};
const md = Setting.settingMetadata(key);
const settingDescription = md.description ? md.description() : '';
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
const containerStyle = this.containerStyle(!!settingDescription);
if (md.isEnum) {
value = value.toString();
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
return (
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View style={containerStyle}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<Dropdown
key="control"
style={this.styles().settingControl}
items={items}
selectedValue={value}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(itemValue: string) => {
void updateSettingValue(key, itemValue);
}}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BOOL) {
return this.renderToggle(key, md.label(), value, updateSettingValue, descriptionComp);
// return (
// <View key={key}>
// <View style={containerStyle}>
// <Text key="label" style={this.styles().switchSettingText}>
// {md.label()}
// </Text>
// <Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value:any) => updateSettingValue(key, value)} />
// </View>
// {descriptionComp}
// </View>
// );
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
const minimum = 'minimum' in md ? md.minimum : 0;
const maximum = 'maximum' in md ? md.maximum : 10;
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
// https://github.com/laurent22/joplin/issues/2733
// https://github.com/react-native-community/react-native-slider/issues/161
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<Text style={this.styles().sliderUnits}>{unitLabel}</Text>
<Slider key="control" style={{ flex: 1 }} step={md.step} minimumValue={minimum} maximumValue={maximum} value={value} onValueChange={value => void updateSettingValue(key, value)} />
</View>
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
return (
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
<View style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<Text style={this.styles().settingControl}>
{this.state.fileSystemSyncPath}
</Text>
</View>
</TouchableNativeFeedback>
);
}
return (
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={key} style={containerStyle}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<TextInput autoCorrect={false} autoComplete="off" selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value: any) => void updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
{descriptionComp}
</View>
);
} else {
// throw new Error('Unsupported setting type: ' + md.type);
}
return output;
return (
<SettingComponent
key={key}
settingId={key}
value={value}
themeId={this.props.themeId}
updateSettingValue={updateSettingValue}
styles={this.styles()}
/>
);
}
private renderFeatureFlags(settings: any, featureFlagKeys: string[]): any[] {
@@ -562,139 +533,77 @@ class ConfigScreenComponent extends BaseScreenComponent {
public render() {
const settings = this.state.settings;
const theme = themeStyle(this.props.themeId);
const showAsSidebar = !this.navigationFillsScreen();
const settingComps = shared.settingsToComponents2(this, 'mobile', settings);
settingComps.push(this.renderHeader('tools', _('Tools')));
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') }));
settingComps.push(this.renderHeader('export', _('Export')));
settingComps.push(<NoteExportButton key={'export_as_jex_button'} styles={this.styles()} />);
if (shim.mobilePlatform() === 'android') {
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
settingComps.push(this.renderButton('export_data', this.state.profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile'), this.exportProfileButtonPress_, { disabled: this.state.profileExportStatus === 'exporting', description: _('For debugging purpose only: export your profile to an external SD card.') }));
if (this.state.profileExportStatus === 'prompt') {
const profileExportPrompt = (
<View style={this.styles().settingContainer} key="profileExport">
<Text style={{ ...this.styles().settingText, flex: 0 }}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20, width: '75%', marginRight: 'auto' }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} />
<Button title="OK" onPress={this.exportProfileButtonPress2_} />
</View>
);
settingComps.push(profileExportPrompt);
}
// If the navigation is a sidebar, always show a section.
let currentSectionName = this.state.selectedSectionName;
if (showAsSidebar && !currentSectionName) {
currentSectionName = 'general';
}
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) {
settingComps.push(this.renderHeader('featureFlags', _('Feature flags')));
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>);
}
const sectionSelector = (
<SectionSelector
selectedSectionName={currentSectionName}
styles={this.styles()}
settings={settings}
openSection={this.switchSectionPress_}
width={this.state.sidebarWidth}
/>
);
settingComps.push(this.renderHeader('moreInfo', _('More information')));
let currentSection: ReactNode;
if (currentSectionName) {
const settingComps = shared.settingsToComponents2(
this, AppType.Mobile, settings, currentSectionName,
if (Platform.OS === 'android' && Platform.Version >= 23) {
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
// TODO: Remove this cast. Currently necessary because of different versions
// of React in lib/ and app-mobile/
) as ReactNode[];
settingComps.push(
<View key="permission_info" style={this.styles().settingContainer}>
<View key="permission_info_wrapper">
<Text key="perm1a" style={this.styles().settingText}>
{_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')}
</Text>
<Text key="perm2" style={this.styles().permissionText}>
{_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')}
</Text>
<Text key="perm3" style={this.styles().permissionText}>
{_('- Camera: to allow taking a picture and attaching it to a note.')}
</Text>
<Text key="perm4" style={this.styles().permissionText}>
{_('- Location: to allow attaching geo-location information to a note.')}
</Text>
</View>
</View>,
currentSection = (
<ScrollView
ref={this.scrollViewRef_}
style={{ flexGrow: 1 }}
>
{settingComps}
</ScrollView>
);
} else {
currentSection = sectionSelector;
}
settingComps.push(
<View key="donate_link" style={this.styles().settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={this.styles().linkText}>
{_('Make a donation')}
</Text>
</TouchableOpacity>
</View>,
);
let mainComponent;
if (showAsSidebar && currentSectionName) {
mainComponent = (
<View style={{
flex: 1,
flexDirection: 'row',
}}>
{sectionSelector}
<View style={{ width: 10 }}/>
{currentSection}
</View>
);
} else {
mainComponent = currentSection;
}
settingComps.push(
<View key="website_link" style={this.styles().settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/');
}}
>
<Text key="label" style={this.styles().linkText}>
{_('Joplin website')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="privacy_link" style={this.styles().settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/privacy/');
}}
>
<Text key="label" style={this.styles().linkText}>
{_('Privacy Policy')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="version_info_app" style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
</View>,
);
settingComps.push(
<View key="version_info_db" style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>{_('Database v%s', reg.db().version())}</Text>
</View>,
);
settingComps.push(
<View key="version_info_fts" style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
</View>,
);
settingComps.push(
<View key="version_info_hermes" style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
</View>,
);
let screenHeadingText = _('Configuration');
if (currentSectionName) {
screenHeadingText = Setting.sectionNameToLabel(currentSectionName);
}
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Configuration')} showSaveButton={true} showSearchButton={false} showSideMenuButton={false} saveButtonDisabled={!this.state.changedSettingKeys.length} onSaveButtonPress={this.saveButton_press} />
<ScrollView ref={this.scrollViewRef_}>{settingComps}</ScrollView>
<ScreenHeader
title={screenHeadingText}
showSaveButton={true}
showSearchButton={false}
showSideMenuButton={false}
saveButtonDisabled={!this.state.changedSettingKeys.length}
onSaveButtonPress={this.saveButton_press}
/>
{mainComponent}
</View>
);
}

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import shim from '@joplin/lib/shim';
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
import { TouchableNativeFeedback, View, Text } from 'react-native';
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import { UpdateSettingValueCallback } from './types';
import { reg } from '@joplin/lib/registry';
interface Props {
styles: ConfigScreenStyles;
settingMetadata: SettingItem;
updateSettingValue: UpdateSettingValueCallback;
}
const FileSystemPathSelector: FunctionComponent<Props> = props => {
const [fileSystemPath, setFileSystemPath] = useState<string>('');
const settingId = props.settingMetadata.key;
useEffect(() => {
setFileSystemPath(Setting.value(settingId));
}, [settingId]);
const selectDirectoryButtonPress = useCallback(async () => {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
setFileSystemPath(doc.uri);
await props.updateSettingValue(settingId, doc.uri);
} else {
throw new Error('User cancelled operation');
}
} catch (e) {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
}, [props.updateSettingValue, settingId]);
// Unsupported on non-Android platforms.
if (!shim.fsDriver().isUsingAndroidSAF()) {
return null;
}
const styleSheet = props.styles.styleSheet;
return (
<TouchableNativeFeedback
onPress={selectDirectoryButtonPress}
style={styleSheet.settingContainer}
>
<View style={styleSheet.settingContainer}>
<Text key="label" style={styleSheet.settingText}>
{props.settingMetadata.label()}
</Text>
<Text style={styleSheet.settingControl}>
{fileSystemPath}
</Text>
</View>
</TouchableNativeFeedback>
);
};
export default FileSystemPathSelector;

View File

@@ -0,0 +1,42 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import { _ } from '@joplin/lib/locale';
import exportDebugReport from './utils/exportDebugReport';
import shim from '@joplin/lib/shim';
import SettingsButton from '../SettingsButton';
import { ConfigScreenStyles } from '../configScreenStyles';
interface Props {
styles: ConfigScreenStyles;
}
const ExportDebugReportButton = (props: Props) => {
const [creatingReport, setCreatingReport] = useState(false);
const exportDebugButtonPress = useCallback(async () => {
setCreatingReport(true);
await exportDebugReport();
setCreatingReport(false);
}, [setCreatingReport]);
const exportDebugReportButton = (
<SettingsButton
title={creatingReport ? _('Creating report...') : _('Export Debug Report')}
clickHandler={exportDebugButtonPress}
styles={props.styles}
disabled={creatingReport}
/>
);
// The debug functionality is only supported on Android.
if (shim.mobilePlatform() !== 'android') {
return null;
}
return exportDebugReportButton;
};
export default ExportDebugReportButton;

View File

@@ -0,0 +1,79 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import { View, Button } from 'react-native';
import { TextInput } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import shim from '@joplin/lib/shim';
import exportProfile from './utils/exportProfile';
import { ConfigScreenStyles } from '../configScreenStyles';
import SettingsButton from '../SettingsButton';
interface Props {
styles: ConfigScreenStyles;
}
const ExportProfileButton = (props: Props) => {
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
const [profileExportPath, setProfileExportPath] = useState<string>('');
const exportProfileButtonPress = useCallback(async () => {
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
return;
}
const p = profileExportPath ? profileExportPath : `${externalDir}/JoplinProfileExport`;
setProfileExportStatus('prompt');
setProfileExportPath(p);
}, [profileExportPath]);
const exportProfileButton = (
<SettingsButton
styles={props.styles}
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile')}
clickHandler={exportProfileButtonPress}
description={_('For debugging purpose only: export your profile to an external SD card.')}
disabled={profileExportStatus === 'exporting'}
/>
);
const exportProfileButtonPress2 = useCallback(async () => {
setProfileExportStatus('exporting');
await exportProfile(profileExportPath);
setProfileExportStatus('idle');
}, [profileExportPath]);
const profileExportPrompt = (
<View>
<TextInput
label={_('Path:')}
onChangeText={text => setProfileExportPath(text)}
value={profileExportPath}
placeholder="/path/to/sdcard"
keyboardAppearance={props.styles.keyboardAppearance} />
<Button
onPress={exportProfileButtonPress2}
title={_('OK')}
/>
</View>
);
const mainContent = (
<>
{exportProfileButton}
{profileExportStatus === 'prompt' ? profileExportPrompt : null}
</>
);
// The debug functionality is only supported on Android.
if (shim.mobilePlatform() !== 'android') {
return null;
}
return mainContent;
};
export default ExportProfileButton;

View File

@@ -7,10 +7,10 @@ import { FunctionComponent, useCallback, useState } from 'react';
import shim from '@joplin/lib/shim';
import { join } from 'path';
import Share from 'react-native-share';
import exportAllFolders, { makeExportCacheDirectory } from './exportAllFolders';
import exportAllFolders, { makeExportCacheDirectory } from './utils/exportAllFolders';
import { ExportProgressState } from '@joplin/lib/services/interop/types';
import { ConfigScreenStyles } from '../configScreenStyles';
import ConfigScreenButton from '../ConfigScreenButton';
import SettingsButton from '../SettingsButton';
const logger = Logger.create('NoteExportButton');
@@ -83,7 +83,7 @@ const NoteExportButton: FunctionComponent<Props> = props => {
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const startOrCancelExportButton = (
<ConfigScreenButton
<SettingsButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
disabled={exportStatus === ExportStatus.Exporting}
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
@@ -96,14 +96,14 @@ const NoteExportButton: FunctionComponent<Props> = props => {
return startOrCancelExportButton;
} else {
const warningComponent = (
<Text style={props.styles.warningText}>
<Text style={props.styles.styleSheet.warningText}>
{_('Warnings:\n%s', warnings)}
</Text>
);
const exportSummary = (
<View style={props.styles.settingContainer}>
<Text style={props.styles.descriptionText}>{_('Exported successfully!')}</Text>
<View style={props.styles.styleSheet.settingContainer}>
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text>
{warnings.length > 0 ? warningComponent : null}
</View>
);

View File

@@ -0,0 +1,32 @@
import { reg } from '@joplin/lib/registry';
import ReportService from '@joplin/lib/services/ReportService';
import shim from '@joplin/lib/shim';
import time from '@joplin/lib/time';
const exportDebugReport = async () => {
const service = new ReportService();
const logItems = await reg.logger().lastEntries(null);
const logItemRows = [['Date', 'Level', 'Message']];
for (let i = 0; i < logItems.length; i++) {
const item = logItems[i];
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
}
const logItemCsv = service.csvCreate(logItemRows);
const itemListCsv = await service.basicItemList({ format: 'csv' });
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
return;
}
const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`;
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
alert(`Debug report exported to ${filePath}`);
};
export default exportDebugReport;

View File

@@ -0,0 +1,35 @@
import shim from '@joplin/lib/shim';
import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
const exportProfile = async (profileExportPath: string) => {
const dbPath = '/data/data/net.cozic.joplin/databases';
const exportPath = profileExportPath;
const resourcePath = `${exportPath}/resources`;
try {
const copyFiles = async (source: string, dest: string) => {
await shim.fsDriver().mkdir(dest);
const files = await shim.fsDriver().readDirStats(source);
for (const file of files) {
const source_ = `${source}/${file.path}`;
const dest_ = `${dest}/${file.path}`;
if (!file.isDirectory()) {
reg.logger().info(`Copying profile: ${source_} => ${dest_}`);
await shim.fsDriver().copy(source_, dest_);
} else {
await copyFiles(source_, dest_);
}
}
};
await copyFiles(dbPath, exportPath);
await copyFiles(Setting.value('resourceDir'), resourcePath);
alert('Profile has been exported!');
} catch (error) {
alert(`Could not export files: ${error.message}`);
}
};
export default exportProfile;

View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import { ConfigScreenStyleSheet } from './configScreenStyles';
import { View, Text, LayoutChangeEvent } from 'react-native';
interface Props {
styles: ConfigScreenStyleSheet;
title: string;
onLayout?: (event: LayoutChangeEvent)=> void;
}
const SectionHeader: React.FunctionComponent<Props> = props => {
return (
<View
style={props.styles.headerWrapperStyle}
onLayout={props.onLayout}
>
<Text style={props.styles.headerTextStyle}>
{props.title}
</Text>
</View>
);
};
export default SectionHeader;

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
import { FlatList, Text, Pressable, View } from 'react-native';
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
import Icon from '../../Icon';
interface Props {
styles: ConfigScreenStyles;
width: number|undefined;
settings: any;
selectedSectionName: string|null;
openSection: (sectionName: string)=> void;
}
const SectionSelector: FunctionComponent<Props> = props => {
const sections = useMemo(() => {
return settingsSections({ device: AppType.Mobile, settings: props.settings });
}, [props.settings]);
const styles = props.styles.styleSheet;
const itemHeight = styles.sidebarButton.height;
const onRenderButton = ({ item }: { item: SettingMetadataSection }) => {
const section = item;
const selected = props.selectedSectionName === section.name;
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
const label = Setting.sectionNameToLabel(section.name);
const shortDescription = Setting.sectionMetadataToSummary(section);
return (
<Pressable
key={section.name}
role='tab'
aria-selected={selected}
onPress={() => props.openSection(section.name)}
style={selected ? styles.selectedSidebarButton : styles.sidebarButton}
>
<Icon
name={icon}
accessibilityLabel={null}
style={styles.sidebarIcon}
/>
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<Text
style={selected ? styles.sidebarSelectedButtonText : styles.sidebarButtonMainText}
>
{label}
</Text>
<Text
style={styles.sidebarButtonDescriptionText}
numberOfLines={2}
ellipsizeMode='tail'
>
{shortDescription ?? ''}
</Text>
</View>
</Pressable>
);
};
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
useEffect(() => {
if (flatListRef && props.selectedSectionName) {
let selectedIndex = 0;
for (const section of sections) {
if (section.name === props.selectedSectionName) {
break;
}
selectedIndex ++;
}
flatListRef.scrollToIndex({
index: selectedIndex,
viewPosition: 0.5,
});
}
}, [props.selectedSectionName, flatListRef, sections]);
return (
<View style={{ width: props.width, flexDirection: 'column' }}>
<FlatList
role='tablist'
ref={setFlatListRef}
data={sections}
renderItem={onRenderButton}
keyExtractor={item => item.name}
getItemLayout={(_data, index) => ({
length: itemHeight, offset: itemHeight * index, index,
})}
/>
</View>
);
};
export default SectionSelector;

View File

@@ -0,0 +1,156 @@
import * as React from 'react';
import { UpdateSettingValueCallback } from './types';
import { View, Text, TextInput } from 'react-native';
import Setting from '@joplin/lib/models/Setting';
import Dropdown from '../../Dropdown';
import { ConfigScreenStyles } from './configScreenStyles';
import Slider from '@react-native-community/slider';
import SettingsToggle from './SettingsToggle';
import FileSystemPathSelector from './FileSystemPathSelector';
import shim from '@joplin/lib/shim';
const { themeStyle } = require('../../global-style.js');
interface Props {
settingId: string;
// The value associated with the given settings key
value: any;
styles: ConfigScreenStyles;
themeId: number;
updateSettingValue: UpdateSettingValueCallback;
}
const SettingComponent: React.FunctionComponent<Props> = props => {
const themeId = props.themeId;
const theme = themeStyle(themeId);
const output: any = null;
const md = Setting.settingMetadata(props.settingId);
const settingDescription = md.description ? md.description() : '';
const styleSheet = props.styles.styleSheet;
const descriptionComp = !settingDescription ? null : <Text style={styleSheet.settingDescriptionText}>{settingDescription}</Text>;
const containerStyle = props.styles.getContainerStyle(!!settingDescription);
if (md.isEnum) {
const value = props.value.toString();
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View style={containerStyle}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<Dropdown
key="control"
items={items as any}
selectedValue={value}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(itemValue: string) => {
void props.updateSettingValue(props.settingId, itemValue);
}}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BOOL) {
return (
<SettingsToggle
settingId={props.settingId}
value={props.value}
themeId={props.themeId}
styles={props.styles}
label={md.label()}
updateSettingValue={props.updateSettingValue}
description={descriptionComp}
/>
);
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(props.value) : props.value;
const minimum = 'minimum' in md ? md.minimum : 0;
const maximum = 'maximum' in md ? md.maximum : 10;
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
// https://github.com/laurent22/joplin/issues/2733
// https://github.com/react-native-community/react-native-slider/issues/161
return (
<View key={props.settingId} style={styleSheet.settingContainer}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<Text style={styleSheet.sliderUnits}>{unitLabel}</Text>
<Slider
key="control"
style={{ flex: 1 }}
step={md.step}
minimumValue={minimum}
maximumValue={maximum}
value={props.value}
onValueChange={newValue => void props.updateSettingValue(props.settingId, newValue)}
/>
</View>
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
return (
<FileSystemPathSelector
styles={props.styles}
settingMetadata={md}
updateSettingValue={props.updateSettingValue}
/>
);
}
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={props.settingId} style={containerStyle}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<TextInput
autoCorrect={false}
autoComplete="off"
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.settingKeyboardAppearance}
autoCapitalize="none"
key="control"
style={styleSheet.settingControl}
value={props.value}
onChangeText={(newValue: string) => void props.updateSettingValue(props.settingId, newValue)}
secureTextEntry={!!md.secure}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BUTTON) {
// TODO: Not yet supported
} else if (Setting.value('env') === 'dev') {
throw new Error(`Unsupported setting type: ${md.type}`);
}
return output;
};
export default SettingComponent;

View File

@@ -6,25 +6,27 @@ import { ConfigScreenStyles } from './configScreenStyles';
interface Props {
title: string;
description: string;
description?: string;
clickHandler: ()=> void;
styles: ConfigScreenStyles;
disabled?: boolean;
statusComponent?: ReactNode;
}
const ConfigScreenButton: FunctionComponent<Props> = props => {
const SettingsButton: FunctionComponent<Props> = props => {
const styles = props.styles.styleSheet;
let descriptionComp = null;
if (props.description) {
descriptionComp = (
<View style={{ flex: 1, marginTop: 10 }}>
<Text style={props.styles.descriptionText}>{props.description}</Text>
<Text style={styles.descriptionText}>{props.description}</Text>
</View>
);
}
return (
<View style={props.styles.settingContainer}>
<View style={styles.settingContainer}>
<View style={{ flex: 1, flexDirection: 'column' }}>
<View style={{ flex: 1 }}>
<Button title={props.title} onPress={props.clickHandler} disabled={!!props.disabled} />
@@ -35,4 +37,4 @@ const ConfigScreenButton: FunctionComponent<Props> = props => {
</View>
);
};
export default ConfigScreenButton;
export default SettingsButton;

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { FunctionComponent, ReactNode } from 'react';
import { View, Text, Switch } from 'react-native';
import { UpdateSettingValueCallback } from './types';
import { themeStyle } from '@joplin/lib/theme';
import { ConfigScreenStyles } from './configScreenStyles';
interface Props {
settingId: string;
value: any;
themeId: number;
styles: ConfigScreenStyles;
label: string;
updateSettingValue: UpdateSettingValueCallback;
description?: ReactNode;
}
const SettingsToggle: FunctionComponent<Props> = props => {
const theme = themeStyle(props.themeId);
const styleSheet = props.styles.styleSheet;
return (
<View>
<View style={props.styles.getContainerStyle(false)}>
<Text key="label" style={styleSheet.switchSettingText}>
{props.label}
</Text>
<Switch
key="control"
style={styleSheet.switchSettingControl}
trackColor={{ false: theme.dividerColor }}
value={props.value}
onValueChange={(value: boolean) => void props.updateSettingValue(props.settingId, value)}
/>
</View>
{props.description}
</View>
);
};
export default SettingsToggle;

View File

@@ -1,13 +1,16 @@
import { TextStyle, ViewStyle, StyleSheet } from 'react-native';
const { themeStyle } = require('../../global-style.js');
export interface ConfigScreenStyles {
type SidebarButtonStyle = ViewStyle & { height: number };
export interface ConfigScreenStyleSheet {
body: ViewStyle;
settingContainer: ViewStyle;
settingContainerNoBottomBorder: ViewStyle;
headerWrapperStyle: ViewStyle;
headerTextStyle: TextStyle;
settingText: TextStyle;
linkText: TextStyle;
descriptionText: TextStyle;
@@ -22,9 +25,24 @@ export interface ConfigScreenStyles {
switchSettingContainer: ViewStyle;
switchSettingControl: TextStyle;
sidebarButton: SidebarButtonStyle;
sidebarIcon: TextStyle;
selectedSidebarButton: SidebarButtonStyle;
sidebarButtonMainText: TextStyle;
sidebarSelectedButtonText: TextStyle;
sidebarButtonDescriptionText: TextStyle;
settingControl: TextStyle;
}
export interface ConfigScreenStyles {
styleSheet: ConfigScreenStyleSheet;
selectedSectionButtonColor: string;
keyboardAppearance: 'default'|'light'|'dark';
getContainerStyle(hasDescription: boolean): ViewStyle;
}
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
const theme = themeStyle(themeId);
@@ -54,7 +72,29 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
borderBottomColor: theme.dividerColor,
};
const styles: ConfigScreenStyles = {
const sidebarButtonHeight = theme.fontSize * 4 + 5;
const sidebarButton: SidebarButtonStyle = {
height: sidebarButtonHeight,
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingEnd: theme.marginRight,
};
const sidebarButtonMainText: TextStyle = {
color: theme.color,
fontSize: theme.fontSize,
};
const sidebarButtonDescriptionText: TextStyle = {
...sidebarButtonMainText,
fontSize: theme.fontSizeSmaller,
color: theme.color,
opacity: 0.75,
};
const styles: ConfigScreenStyleSheet = {
body: {
flex: 1,
justifyContent: 'flex-start',
@@ -119,6 +159,8 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
justifyContent: 'space-between',
},
headerTextStyle: theme.headerStyle,
headerWrapperStyle: {
...settingContainerStyle,
...theme.headerWrapperStyle,
@@ -129,9 +171,39 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
color: undefined,
flex: 0,
},
sidebarButton,
selectedSidebarButton: {
...sidebarButton,
backgroundColor: theme.selectedColor,
},
sidebarButtonMainText: sidebarButtonMainText,
sidebarIcon: {
...sidebarButtonMainText,
textAlign: 'center',
fontSize: 18,
width: sidebarButtonHeight * 0.8,
},
sidebarSelectedButtonText: {
...sidebarButtonMainText,
fontWeight: 'bold',
},
sidebarButtonDescriptionText,
};
return StyleSheet.create(styles);
const styleSheet = StyleSheet.create(styles);
return {
styleSheet,
selectedSectionButtonColor: theme.selectedColor,
keyboardAppearance: theme.keyboardAppearance,
getContainerStyle: (hasDescription) => {
return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder;
},
};
};
export default configScreenStyles;

View File

@@ -0,0 +1,10 @@
import { ReactElement } from 'react';
export interface CustomSettingSection {
component: ReactElement;
icon: string;
title: string;
keywords: string[];
}
export type UpdateSettingValueCallback = (key: string, value: any)=> Promise<void>;

View File

@@ -32,7 +32,7 @@ const { Checkbox } = require('../checkbox.js');
import { _, currentLocale } from '@joplin/lib/locale';
import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const { themeStyle, editorFont } = require('../global-style.js');
const { dialogs } = require('../../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
@@ -51,6 +51,7 @@ import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
import { voskEnabled } from '../../services/voiceTyping/vosk';
import { isSupportedLanguage } from '../../services/voiceTyping/vosk.android';
import { ChangeEvent as EditorChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { join } from 'path';
const urlUtils = require('@joplin/lib/urlUtils');
// import Vosk from 'react-native-vosk';
@@ -726,7 +727,7 @@ class NoteScreenComponent extends BaseScreenComponent {
resource = await Resource.save(resource, { isNew: true });
const resourceTag = Resource.markdownTag(resource);
const resourceTag = Resource.markupTag(resource);
const newNote = { ...this.state.note };
@@ -814,12 +815,7 @@ class NoteScreenComponent extends BaseScreenComponent {
private drawPicture_onPress = async () => {
// Create a new empty drawing and attach it now.
const resource = await this.attachNewDrawing('');
this.setState({
showImageEditor: true,
loadImageEditorData: null,
imageEditorResource: resource,
});
await this.editDrawing(resource);
};
private async updateDrawing(svgData: string) {
@@ -829,13 +825,17 @@ class NoteScreenComponent extends BaseScreenComponent {
throw new Error('No resource is loaded in the editor');
}
const resourcePath = Resource.fullPath(resource);
logger.info('Saving drawing to resource', resource.id);
const filePath = resourcePath;
await shim.fsDriver().writeFile(filePath, svgData, 'utf8');
logger.info('Saved drawing to', filePath);
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
resource = await Resource.updateResourceBlobContent(
resource.id,
tempFilePath,
);
await shim.fsDriver().remove(tempFilePath);
resource = await Resource.save(resource, { isNew: false });
await this.refreshResource(resource);
}

View File

@@ -13,7 +13,7 @@ import { _ } from '@joplin/lib/locale';
import ActionButton from '../ActionButton';
const { dialogs } = require('../../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const { BackButtonService } = require('../../services/back-button.js');
import { AppState } from '../../utils/types';

View File

@@ -4,7 +4,7 @@ const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView
const { connect } = require('react-redux');
const { ScreenHeader } = require('../ScreenHeader');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');

View File

@@ -291,10 +291,10 @@ const EncryptionConfigScreen = (props: Props) => {
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
Linking.openURL('https://joplinapp.org/help/apps/sync/e2ee');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
<Text>https://joplinapp.org/help/apps/sync/e2ee</Text>
</TouchableOpacity>
</View>
}

View File

@@ -5,7 +5,7 @@ const { connect } = require('react-redux');
const Folder = require('@joplin/lib/models/Folder').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const { ScreenHeader } = require('../ScreenHeader');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const { dialogs } = require('../../utils/dialogs.js');
const { _ } = require('@joplin/lib/locale');
const { default: FolderPicker } = require('../FolderPicker');

View File

@@ -7,7 +7,7 @@ const { connect } = require('react-redux');
const { ScreenHeader } = require('../ScreenHeader');
const { reg } = require('@joplin/lib/registry.js');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const parseUri = require('@joplin/lib/parseUri');
const { themeStyle } = require('../global-style.js');
const shim = require('@joplin/lib/shim').default;

View File

@@ -7,7 +7,7 @@ const Icon = require('react-native-vector-icons/Ionicons').default;
import { _ } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
const { NoteItem } = require('../note-item.js');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
import SearchEngineUtils from '@joplin/lib/services/searchengine/SearchEngineUtils';
@@ -98,7 +98,7 @@ class SearchScreenComponent extends BaseScreenComponent {
if (query) {
if (this.props.settings['db.ftsEnabled']) {
notes = await SearchEngineUtils.notesForQuery(query, true);
notes = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
} else {
const p = query.split(' ');
const temp = [];

View File

@@ -6,7 +6,7 @@ const { connect } = require('react-redux');
const { ScreenHeader } = require('../ScreenHeader');
const ReportService = require('@joplin/lib/services/ReportService').default;
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
const { themeStyle } = require('../global-style.js');
class StatusScreenComponent extends BaseScreenComponent {

View File

@@ -6,7 +6,7 @@ const Tag = require('@joplin/lib/models/Tag').default;
const { themeStyle } = require('../global-style.js');
const { ScreenHeader } = require('../ScreenHeader');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const { BaseScreenComponent } = require('../base-screen');
class TagsScreenComponent extends BaseScreenComponent {
static navigationOptions() {

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