You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
144 Commits
android-v2
...
v3.0.2
Author | SHA1 | Date | |
---|---|---|---|
|
a59ad20bd5 | ||
|
e3de158d18 | ||
|
e9514e742b | ||
|
d2c060cd97 | ||
|
57fc70cec1 | ||
|
180f52dab2 | ||
|
0c6df3dd73 | ||
|
b29bf7de5d | ||
|
382f0d8218 | ||
|
55d72a8f68 | ||
|
e9ebd845b9 | ||
|
ea29cf4e13 | ||
|
d260d0efce | ||
|
e92f89df99 | ||
|
44e8950f1b | ||
|
32141d4e23 | ||
|
7d068cfb87 | ||
|
9c3e751ebc | ||
|
eecad1aefc | ||
|
eb06ac673b | ||
|
5e2c54f2ad | ||
|
15649c89f1 | ||
|
42483a4d46 | ||
|
56b010ba0e | ||
|
cfd98e3a4d | ||
|
40db753417 | ||
|
3d2c100fe9 | ||
|
fd4d7ead43 | ||
|
073df50244 | ||
|
6973734d5b | ||
|
b44b30124c | ||
|
60f447dd49 | ||
|
7638bdf171 | ||
|
9dc480e8d1 | ||
|
c2dbb9606f | ||
|
a40c3b792e | ||
|
08aa2ae939 | ||
|
85d98f5254 | ||
|
310a90744a | ||
|
b3ec92a57e | ||
|
b9eb4522f5 | ||
|
04298f0eba | ||
|
a53a8d67a1 | ||
|
6467bf0fc1 | ||
|
4ac0cdf556 | ||
|
3e34f150b8 | ||
|
e72cce0d07 | ||
|
78b8839ae3 | ||
|
49cd17e520 | ||
|
c16ce1c434 | ||
|
8eea3953f3 | ||
|
8cb9c08bcb | ||
|
bc7a0fa095 | ||
|
298549e51a | ||
|
da393f6c34 | ||
|
8bdac6ffbf | ||
|
55cafb8891 | ||
|
238468ddaa | ||
|
b152732d7f | ||
|
56dde88003 | ||
|
9e0a0468b2 | ||
|
e203397f89 | ||
|
3e22041672 | ||
|
0d018a8d7a | ||
|
c3954d7326 | ||
|
d7401d70a7 | ||
|
bae16f7a65 | ||
|
25cd5affca | ||
|
91004f5714 | ||
|
c35085d1d5 | ||
|
17a8ce5010 | ||
|
4d8fcff6d5 | ||
|
75cb639ed2 | ||
|
3222b620b9 | ||
|
d7a0d74c4d | ||
|
52810c51f5 | ||
|
1b96a16586 | ||
|
971c4e5e84 | ||
|
9ef0a504ec | ||
|
e59211deea | ||
|
3177729663 | ||
|
70c7804a43 | ||
|
c40682f16f | ||
|
406c778cfd | ||
|
9d17ab429d | ||
|
20f8bb76f7 | ||
|
5e4c35a18f | ||
|
9a6484c488 | ||
|
1dfebf5ed3 | ||
|
be2f4d3d79 | ||
|
a1cea6776f | ||
|
3c10282848 | ||
|
d9a16b5c0f | ||
|
28c7268f82 | ||
|
3eab87ae69 | ||
|
9acbac6613 | ||
|
9a10cd4bec | ||
|
5aba1e38a2 | ||
|
b812027281 | ||
|
dfc08da40c | ||
|
f5f47f3c08 | ||
|
8d5ee36745 | ||
|
7068670554 | ||
|
6e3162f92f | ||
|
6494b74d0c | ||
|
d26d9f16d9 | ||
|
4c6969b17d | ||
|
9a2a251eec | ||
|
f6c7213f69 | ||
|
4827d0bf92 | ||
|
f0f6590312 | ||
|
fa83d48141 | ||
|
c409160ad7 | ||
|
ff1f1b190e | ||
|
53d5cf55bc | ||
|
f19b1c5364 | ||
|
07fbd547dc | ||
|
cb540a5abb | ||
|
a7b303259c | ||
|
ee181c1fd6 | ||
|
2e8fc99c5c | ||
|
4a78cd2564 | ||
|
95e42c4ca7 | ||
|
98bb0250f2 | ||
|
5aba5e544d | ||
|
0ca36bbf66 | ||
|
5d3034d418 | ||
|
438dddda6e | ||
|
8bd6132398 | ||
|
d1c6c0622b | ||
|
bd5b3feabe | ||
|
22f4d19dd1 | ||
|
a86f859b42 | ||
|
4bef8aa632 | ||
|
40ae03c438 | ||
|
afedc53354 | ||
|
0d5bca20d3 | ||
|
f254255ba5 | ||
|
fc1c1a3c20 | ||
|
3c31b2bc38 | ||
|
b4cc6803e7 | ||
|
2ac7997c07 | ||
|
0055345689 | ||
|
f43f5c0a34 |
172
.eslintignore
172
.eslintignore
@@ -52,7 +52,7 @@ packages/app-desktop/packageInfo.js
|
||||
packages/app-desktop/services/electron-context-menu.js
|
||||
packages/app-desktop/vendor/lib/
|
||||
packages/app-mobile/android
|
||||
packages/app-mobile/components/NoteEditor/**/*.bundle.js
|
||||
packages/app-mobile/**/*.bundle.js
|
||||
packages/app-mobile/ios
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/locales
|
||||
@@ -109,7 +109,10 @@ packages/app-cli/app/command-mkbook.test.js
|
||||
packages/app-cli/app/command-mkbook.js
|
||||
packages/app-cli/app/command-mv.js
|
||||
packages/app-cli/app/command-ren.js
|
||||
packages/app-cli/app/command-restore.js
|
||||
packages/app-cli/app/command-rmbook.test.js
|
||||
packages/app-cli/app/command-rmbook.js
|
||||
packages/app-cli/app/command-rmnote.test.js
|
||||
packages/app-cli/app/command-rmnote.js
|
||||
packages/app-cli/app/command-set.js
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
@@ -117,6 +120,7 @@ packages/app-cli/app/command-sync.js
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-use.js
|
||||
packages/app-cli/app/command-version.js
|
||||
packages/app-cli/app/gui/FolderListWidget.js
|
||||
packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
@@ -143,6 +147,7 @@ packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/emptyTrash.js
|
||||
packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
@@ -168,8 +173,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@@ -188,6 +191,7 @@ packages/app-desktop/gui/IconButton.js
|
||||
packages/app-desktop/gui/ImportScreen.js
|
||||
packages/app-desktop/gui/ItemList.js
|
||||
packages/app-desktop/gui/JoplinCloudConfigScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudLoginScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||
packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
@@ -217,10 +221,13 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/resetLayout.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/search.js
|
||||
packages/app-desktop/gui/MainScreen/commands/setTags.js
|
||||
@@ -248,26 +255,30 @@ packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -279,6 +290,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
@@ -329,9 +341,19 @@ packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListHeader/NoteListHeader.js
|
||||
packages/app-desktop/gui/NoteListHeader/NoteListHeaderItem.js
|
||||
packages/app-desktop/gui/NoteListHeader/types.js
|
||||
packages/app-desktop/gui/NoteListHeader/useDragAndDrop.test.js
|
||||
packages/app-desktop/gui/NoteListHeader/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/getColumnTitle.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
@@ -346,6 +368,7 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
@@ -390,6 +413,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -405,6 +429,7 @@ packages/app-desktop/gui/style/StyledMessage.js
|
||||
packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/dragAndDrop.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gulpfile.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
@@ -456,6 +481,11 @@ 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/commands/index.js
|
||||
packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
@@ -467,13 +497,22 @@ 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/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/noteBodyViewerBundle.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/types.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/utils/addPluginAssets.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/utils/makeResourceModel.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useContentScripts.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useRenderer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useRerenderHandler.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteBodyViewer/types.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
@@ -495,10 +534,15 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButto
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
@@ -516,6 +560,7 @@ 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/global-style.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
@@ -532,7 +577,19 @@ 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/plugins/PluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/isPluginInstallingAllowed.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -542,6 +599,28 @@ packages/app-mobile/components/screens/search.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
@@ -562,10 +641,23 @@ packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver/tarCreate.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.test.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/default-plugins/build.js
|
||||
packages/default-plugins/buildDefaultPlugins.js
|
||||
@@ -575,6 +667,7 @@ packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
|
||||
@@ -583,10 +676,15 @@ packages/editor/CodeMirror/CodeMirrorControl.js
|
||||
packages/editor/CodeMirror/configFromSettings.js
|
||||
packages/editor/CodeMirror/createEditor.test.js
|
||||
packages/editor/CodeMirror/createEditor.js
|
||||
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
||||
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.test.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -613,6 +711,7 @@ packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
@@ -639,6 +738,7 @@ packages/generator-joplin/generators/app/templates/api/noteListType.js
|
||||
packages/generator-joplin/generators/app/templates/api/types.js
|
||||
packages/generator-joplin/generators/app/templates/api_index.js
|
||||
packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
@@ -678,16 +778,22 @@ 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/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.test.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/components/shared/side-menu-shared.test.js
|
||||
packages/lib/components/shared/side-menu-shared.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
packages/lib/database.js
|
||||
packages/lib/debug/DebugService.js
|
||||
packages/lib/determineBaseAppDirs.js
|
||||
packages/lib/dom.js
|
||||
packages/lib/downloadController.js
|
||||
packages/lib/errorUtils.js
|
||||
packages/lib/errors.js
|
||||
packages/lib/eventManager.js
|
||||
@@ -704,6 +810,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
@@ -749,10 +856,15 @@ packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/onFolderDrop.test.js
|
||||
packages/lib/models/utils/onFolderDrop.js
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginationToSql.js
|
||||
packages/lib/models/utils/readOnly.test.js
|
||||
packages/lib/models/utils/readOnly.js
|
||||
packages/lib/models/utils/resourceUtils.js
|
||||
packages/lib/models/utils/types.js
|
||||
@@ -848,6 +960,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
@@ -856,6 +969,12 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
|
||||
packages/lib/services/noteList/defaultListRenderer.js
|
||||
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
|
||||
packages/lib/services/noteList/depNameToNoteProp.js
|
||||
packages/lib/services/noteList/renderTemplate.test.js
|
||||
packages/lib/services/noteList/renderTemplate.js
|
||||
packages/lib/services/noteList/renderViewProps.test.js
|
||||
packages/lib/services/noteList/renderViewProps.js
|
||||
packages/lib/services/noteList/renderers.js
|
||||
packages/lib/services/ocr/OcrDriverBase.js
|
||||
packages/lib/services/ocr/OcrService.test.js
|
||||
@@ -923,6 +1042,7 @@ packages/lib/services/rest/actionApi.desktop.js
|
||||
packages/lib/services/rest/routes/auth.js
|
||||
packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/folders.test.js
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.test.js
|
||||
@@ -991,6 +1111,17 @@ packages/lib/services/synchronizer/utils/handleSyncStartupOperation.js
|
||||
packages/lib/services/synchronizer/utils/resourceRemotePath.js
|
||||
packages/lib/services/synchronizer/utils/syncDeleteStep.js
|
||||
packages/lib/services/synchronizer/utils/types.js
|
||||
packages/lib/services/trash/emptyTrash.test.js
|
||||
packages/lib/services/trash/emptyTrash.js
|
||||
packages/lib/services/trash/getTrashFolderId.js
|
||||
packages/lib/services/trash/index.test.js
|
||||
packages/lib/services/trash/index.js
|
||||
packages/lib/services/trash/isTrashableItem.js
|
||||
packages/lib/services/trash/isTrashableNoteOrFolder.js
|
||||
packages/lib/services/trash/permanentlyDeleteOldItems.test.js
|
||||
packages/lib/services/trash/permanentlyDeleteOldItems.js
|
||||
packages/lib/services/trash/restoreItems.test.js
|
||||
packages/lib/services/trash/restoreItems.js
|
||||
packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
@@ -1010,7 +1141,19 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/types.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
packages/lib/utils/ipc/WindowMessenger.js
|
||||
packages/lib/utils/ipc/types.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
@@ -1113,6 +1256,7 @@ packages/tools/packageJsonLint.js
|
||||
packages/tools/postPreReleasesToForum.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
packages/tools/release-clipper.js
|
||||
packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
|
58
.github/scripts/run_ci.sh
vendored
58
.github/scripts/run_ci.sh
vendored
@@ -141,15 +141,13 @@ fi
|
||||
# for Linux only is sufficient.
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Validating translations..."
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Validating translations..."
|
||||
|
||||
node packages/tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
node packages/tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -179,15 +177,13 @@ fi
|
||||
# See coding_style.md
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Checking for files that should have been ignored..."
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Checking for files that should have been ignored..."
|
||||
|
||||
node packages/tools/checkIgnoredFiles.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
node packages/tools/checkIgnoredFiles.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -196,14 +192,16 @@ fi
|
||||
# =============================================================================
|
||||
|
||||
if [ "$RUN_TESTS" == "1" ]; then
|
||||
echo "Step: Check that the website still builds..."
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Check that the website still builds..."
|
||||
|
||||
mkdir -p ../joplin-website/docs
|
||||
ll ../joplin-website/docs/api/references/plugin_api
|
||||
SKIP_SPONSOR_PROCESSING=1 yarn buildWebsite
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
mkdir -p ../joplin-website/docs
|
||||
CROWDIN_PERSONAL_TOKEN="$CROWDIN_PERSONAL_TOKEN" yarn crowdinDownload
|
||||
SKIP_SPONSOR_PROCESSING=1 yarn buildWebsite
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -211,15 +209,13 @@ fi
|
||||
# Spellchecking
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Spellchecking..."
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Step: Spellchecking..."
|
||||
|
||||
yarn spellcheck --all
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
yarn spellcheck --all
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
|
1
.github/workflows/github-actions-main.yml
vendored
1
.github/workflows/github-actions-main.yml
vendored
@@ -127,6 +127,7 @@ jobs:
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SERVER_REPOSITORY: joplin/server
|
||||
SERVER_TAG_PREFIX: server
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
|
||||
|
||||
|
170
.gitignore
vendored
170
.gitignore
vendored
@@ -89,7 +89,10 @@ packages/app-cli/app/command-mkbook.test.js
|
||||
packages/app-cli/app/command-mkbook.js
|
||||
packages/app-cli/app/command-mv.js
|
||||
packages/app-cli/app/command-ren.js
|
||||
packages/app-cli/app/command-restore.js
|
||||
packages/app-cli/app/command-rmbook.test.js
|
||||
packages/app-cli/app/command-rmbook.js
|
||||
packages/app-cli/app/command-rmnote.test.js
|
||||
packages/app-cli/app/command-rmnote.js
|
||||
packages/app-cli/app/command-set.js
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
@@ -97,6 +100,7 @@ packages/app-cli/app/command-sync.js
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-use.js
|
||||
packages/app-cli/app/command-version.js
|
||||
packages/app-cli/app/gui/FolderListWidget.js
|
||||
packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
@@ -123,6 +127,7 @@ packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/emptyTrash.js
|
||||
packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
@@ -148,8 +153,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@@ -168,6 +171,7 @@ packages/app-desktop/gui/IconButton.js
|
||||
packages/app-desktop/gui/ImportScreen.js
|
||||
packages/app-desktop/gui/ItemList.js
|
||||
packages/app-desktop/gui/JoplinCloudConfigScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudLoginScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||
packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
@@ -197,10 +201,13 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/resetLayout.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/search.js
|
||||
packages/app-desktop/gui/MainScreen/commands/setTags.js
|
||||
@@ -228,26 +235,30 @@ packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -259,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
@@ -309,9 +321,19 @@ packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListHeader/NoteListHeader.js
|
||||
packages/app-desktop/gui/NoteListHeader/NoteListHeaderItem.js
|
||||
packages/app-desktop/gui/NoteListHeader/types.js
|
||||
packages/app-desktop/gui/NoteListHeader/useDragAndDrop.test.js
|
||||
packages/app-desktop/gui/NoteListHeader/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/getColumnTitle.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
|
||||
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
@@ -326,6 +348,7 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
@@ -370,6 +393,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -385,6 +409,7 @@ packages/app-desktop/gui/style/StyledMessage.js
|
||||
packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/dragAndDrop.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gulpfile.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
@@ -436,6 +461,11 @@ 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/commands/index.js
|
||||
packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
@@ -447,13 +477,22 @@ 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/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/noteBodyViewerBundle.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/types.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/utils/addPluginAssets.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/utils/makeResourceModel.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useContentScripts.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useRenderer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useRerenderHandler.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteBodyViewer/types.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
@@ -475,10 +514,15 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButto
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
@@ -496,6 +540,7 @@ 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/global-style.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
@@ -512,7 +557,19 @@ 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/plugins/PluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/isPluginInstallingAllowed.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -522,6 +579,28 @@ packages/app-mobile/components/screens/search.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
@@ -542,10 +621,23 @@ packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver/tarCreate.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.test.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/default-plugins/build.js
|
||||
packages/default-plugins/buildDefaultPlugins.js
|
||||
@@ -555,6 +647,7 @@ packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
|
||||
@@ -563,10 +656,15 @@ packages/editor/CodeMirror/CodeMirrorControl.js
|
||||
packages/editor/CodeMirror/configFromSettings.js
|
||||
packages/editor/CodeMirror/createEditor.test.js
|
||||
packages/editor/CodeMirror/createEditor.js
|
||||
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
||||
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.test.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -593,6 +691,7 @@ packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
@@ -619,6 +718,7 @@ packages/generator-joplin/generators/app/templates/api/noteListType.js
|
||||
packages/generator-joplin/generators/app/templates/api/types.js
|
||||
packages/generator-joplin/generators/app/templates/api_index.js
|
||||
packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
@@ -658,16 +758,22 @@ 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/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.test.js
|
||||
packages/lib/components/shared/config/plugins/useOnInstallHandler.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/components/shared/side-menu-shared.test.js
|
||||
packages/lib/components/shared/side-menu-shared.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
packages/lib/database.js
|
||||
packages/lib/debug/DebugService.js
|
||||
packages/lib/determineBaseAppDirs.js
|
||||
packages/lib/dom.js
|
||||
packages/lib/downloadController.js
|
||||
packages/lib/errorUtils.js
|
||||
packages/lib/errors.js
|
||||
packages/lib/eventManager.js
|
||||
@@ -684,6 +790,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
@@ -729,10 +836,15 @@ packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/onFolderDrop.test.js
|
||||
packages/lib/models/utils/onFolderDrop.js
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginationToSql.js
|
||||
packages/lib/models/utils/readOnly.test.js
|
||||
packages/lib/models/utils/readOnly.js
|
||||
packages/lib/models/utils/resourceUtils.js
|
||||
packages/lib/models/utils/types.js
|
||||
@@ -828,6 +940,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
@@ -836,6 +949,12 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
|
||||
packages/lib/services/noteList/defaultListRenderer.js
|
||||
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
|
||||
packages/lib/services/noteList/depNameToNoteProp.js
|
||||
packages/lib/services/noteList/renderTemplate.test.js
|
||||
packages/lib/services/noteList/renderTemplate.js
|
||||
packages/lib/services/noteList/renderViewProps.test.js
|
||||
packages/lib/services/noteList/renderViewProps.js
|
||||
packages/lib/services/noteList/renderers.js
|
||||
packages/lib/services/ocr/OcrDriverBase.js
|
||||
packages/lib/services/ocr/OcrService.test.js
|
||||
@@ -903,6 +1022,7 @@ packages/lib/services/rest/actionApi.desktop.js
|
||||
packages/lib/services/rest/routes/auth.js
|
||||
packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/folders.test.js
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.test.js
|
||||
@@ -971,6 +1091,17 @@ packages/lib/services/synchronizer/utils/handleSyncStartupOperation.js
|
||||
packages/lib/services/synchronizer/utils/resourceRemotePath.js
|
||||
packages/lib/services/synchronizer/utils/syncDeleteStep.js
|
||||
packages/lib/services/synchronizer/utils/types.js
|
||||
packages/lib/services/trash/emptyTrash.test.js
|
||||
packages/lib/services/trash/emptyTrash.js
|
||||
packages/lib/services/trash/getTrashFolderId.js
|
||||
packages/lib/services/trash/index.test.js
|
||||
packages/lib/services/trash/index.js
|
||||
packages/lib/services/trash/isTrashableItem.js
|
||||
packages/lib/services/trash/isTrashableNoteOrFolder.js
|
||||
packages/lib/services/trash/permanentlyDeleteOldItems.test.js
|
||||
packages/lib/services/trash/permanentlyDeleteOldItems.js
|
||||
packages/lib/services/trash/restoreItems.test.js
|
||||
packages/lib/services/trash/restoreItems.js
|
||||
packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
@@ -990,7 +1121,19 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/types.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
packages/lib/utils/ipc/WindowMessenger.js
|
||||
packages/lib/utils/ipc/types.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
@@ -1093,6 +1236,7 @@ packages/tools/packageJsonLint.js
|
||||
packages/tools/postPreReleasesToForum.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
packages/tools/release-clipper.js
|
||||
packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
|
33
.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch
Normal file
33
.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch
Normal file
@@ -0,0 +1,33 @@
|
||||
diff --git a/lib/runner/index.js b/lib/runner/index.js
|
||||
index 87e3b3957619728e3ed1ca61e2d83df1c49f928f..6d5ab905415da0577341c8f5b67d4806adcf7549 100644
|
||||
--- a/lib/runner/index.js
|
||||
+++ b/lib/runner/index.js
|
||||
@@ -68,15 +68,19 @@ function run([, scriptPath, hookName = '', HUSKY_GIT_PARAMS], getStdinFn = get_s
|
||||
return 0;
|
||||
}
|
||||
catch (err) {
|
||||
- const noVerifyMessage = [
|
||||
- 'commit-msg',
|
||||
- 'pre-commit',
|
||||
- 'pre-rebase',
|
||||
- 'pre-push'
|
||||
- ].includes(hookName)
|
||||
- ? '(add --no-verify to bypass)'
|
||||
- : '(cannot be bypassed with --no-verify due to Git specs)';
|
||||
- console.log(`husky > ${hookName} hook failed ${noVerifyMessage}`);
|
||||
+ // We do not want to print this "add --no-verify to bypass" message because that's
|
||||
+ // literally what some developers do instead of trying to fix the errors.
|
||||
+
|
||||
+ // const noVerifyMessage = [
|
||||
+ // 'commit-msg',
|
||||
+ // 'pre-commit',
|
||||
+ // 'pre-rebase',
|
||||
+ // 'pre-push'
|
||||
+ // ].includes(hookName)
|
||||
+ // ? '(add --no-verify to bypass)'
|
||||
+ // : '(cannot be bypassed with --no-verify due to Git specs)';
|
||||
+
|
||||
+ console.log(`husky > ${hookName} hook failed (Please fix the errors listed above and try again)`);
|
||||
return err.code;
|
||||
}
|
||||
});
|
BIN
Assets/WebsiteAssets/images/news/20240301-rte-colors.png
Normal file
BIN
Assets/WebsiteAssets/images/news/20240301-rte-colors.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Sat, 27 Jan 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Sat, 27 Jan 2024 00:00:00 GMT</pubDate><item><title><![CDATA[Support for new plugin metadata]]></title><description><![CDATA[<p>The plugin manifest now supports new properties to better describe and present your plugins on Joplin Plugins website. Those are the <code>icons</code>, <code>categories</code>, <code>screenshots</code> and <code>promo_tile</code> properties.</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Fri, 01 Mar 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
|
||||
<p>Optical Character Recognition (OCR) in Joplin enables the transformation of text-containing images into machine-readable text formats. From this version you can enable OCR in the Configuration screen under the "General" section. Once activated, Joplin scans images and PDFs, extracting text data for searchability.</p>
|
||||
<p>While OCR search is available on both desktop and mobile apps, document scanning is limited to the desktop due to resource demands. For more information head to the <a href="https://joplinapp.org/help/apps/ocr">OCR official documentation</a>!</p>
|
||||
<h2>Bundled plugins<a name="bundled-plugins" href="#bundled-plugins" class="heading-anchor">🔗</a></h2>
|
||||
<p>Joplin will now bundle high quality plugins that we feel will benefit most users. With this version we include the great <a href="https://github.com/JackGruber/joplin-plugin-backup">Backup plugin</a> by JackGruber. This will provide another layer of safety when using Joplin as by default it will automatically backup your notes in a "JoplinBackup" folder in your home directory.</p>
|
||||
<p>Note that, just like any other plugin, you can change the plugin configuration or even disable it from the settings.</p>
|
||||
<h2>ENEX importer<a name="enex-importer" href="#enex-importer" class="heading-anchor">🔗</a></h2>
|
||||
<p>As usual in recent version, there are plenty of improvements to the <a href="https://joplinapp.org/help/apps/import_export#importing-from-evernote">Joplin ENEX importer</a>. Besides the various fixes and enhancement to support this format, we've added a few useful features:</p>
|
||||
<h3>Restore note links after importing an ENEX file<a name="restore-note-links-after-importing-an-enex-file" href="#restore-note-links-after-importing-an-enex-file" class="heading-anchor">🔗</a></h3>
|
||||
<p>Evernote Export files do not include the necessary information to reliably restore the links between notes, so for a long time this feature was not supported by the importer.</p>
|
||||
<p>Now however Joplin will try to guess what note is linked to what other note based on the note title, which in many cases will give the expected result. But not always - when that happens, and Joplin cannot detect the link target, the application leaves the original Evernote link. That way you can manually restore it yourself or at least find back what the note was linked to.</p>
|
||||
<h3>Import a directory of ENEX files<a name="import-a-directory-of-enex-files" href="#import-a-directory-of-enex-files" class="heading-anchor">🔗</a></h3>
|
||||
<p>It is notoriously difficult to export data from Evernote because, among other issues, you can only export one notebook at a time, which is an obvious problems when you have dozens of notebooks. Unfortunately we cannot improve this part of the process since this up to Evernote, however we now make it easier to import all these notebook files by adding support for importing a folder of ENEX files. To use this feature, go to File > Import, and select one of the "ENEX (Directory)" options.</p>
|
||||
<p>This will process all the ENEX files in that directory and create a notebook in Joplin for each of them.</p>
|
||||
<h2>Beta Markdown editor<a name="beta-markdown-editor" href="#beta-markdown-editor" class="heading-anchor">🔗</a></h2>
|
||||
<p>This version features further improvements to the new Markdown editor based on <a href="https://codemirror.net/">CodeMirror 6</a>. The goal eventually is to be able to use the same editor on both the desktop and mobile application (which already uses CodeMirror 6), which will allow a more consistent user experience across devices.</p>
|
||||
<p>Plugin support has also been improved in this version - plugin authors can now write native CodeMirror 6 extensions using the plugin API. For more information check the documentation on <a href="https://joplinapp.org/help/api/tutorials/cm6_plugin/">how to create a Markdown plugin</a>!</p>
|
||||
<p>Another benefit of this new editor is that, in a future version, it will allow us to support plugins on the mobile application since a plugin written for the desktop app will work on mobile too. There are several other advantages that Henry <a href="https://discourse.joplinapp.org/t/pre-release-v2-13-is-now-available-updated-18-11-2023/32697/12?u=laurent">listed in this forum post</a>.</p>
|
||||
<h2>Rich text editor improvements<a name="rich-text-editor-improvements" href="#rich-text-editor-improvements" class="heading-anchor">🔗</a></h2>
|
||||
<p>We continue making improvements to the Rich Text Editor (RTE) in particular to improve interoperability with other applications, such as LibreOffice, Office or web browsers, as well as better handling of copy and paste.</p>
|
||||
<p>Another notable addition is support for setting colours, which was a frequently asked feature. To use the feature, select it from the "..." button in the toolbar. Note that once applied the colours will work in the Markdown editor too!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240301-rte-colors.png" alt=""></p>
|
||||
<p>See below for the full list of RTE changes:</p>
|
||||
<ul>
|
||||
<li>Fixed: Rich text editor: Fix context menu not shown in some cases</li>
|
||||
<li>Improved: Speed up pasting text and images in Rich Text Editor</li>
|
||||
<li>Fixed: Fix drag-and-drop of images and text in the rich text editor</li>
|
||||
<li>Fixed: Fix images with SVG data URLs corrupted in the rich text editor</li>
|
||||
<li>Fixed: Pasting rich text in the RTE sometimes result in invalid markup</li>
|
||||
<li>Fixed: Rich text editor: Fix newline behavior in new notes</li>
|
||||
<li>Improved: Add support for changing text colors in rich text editor</li>
|
||||
<li>Fixed: Fix HTML resource links lost when editing notes in the rich text editor</li>
|
||||
<li>Fixed: Fix code blocks with blank lines break tables in the rich text editor</li>
|
||||
<li>Fixed: Copied and pasted text from Firefox to RTE does not include images</li>
|
||||
<li>Fixed: Pasting rich text in the RTE sometimes result in invalid markup</li>
|
||||
<li>Fixed: Fixed copying and pasting an image from Chrome in RTE</li>
|
||||
</ul>
|
||||
<h1>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h1>
|
||||
<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/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/20240301-release-2-14</link><guid isPermaLink="false">20240301-release-2-14</guid><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.14</twitter-text></item><item><title><![CDATA[Support for new plugin metadata]]></title><description><![CDATA[<p>The plugin manifest now supports new properties to better describe and present your plugins on Joplin Plugins website. Those are the <code>icons</code>, <code>categories</code>, <code>screenshots</code> and <code>promo_tile</code> properties.</p>
|
||||
<h2>Icon<a name="icon" href="#icon" class="heading-anchor">🔗</a></h2>
|
||||
<p>This is the icon that will be used in various plugin pages, including in your main plugin page. It will be shown on the main result page too. If not provided, a default icon will be displayed instead.</p>
|
||||
<h2>Category<a name="category" href="#category" class="heading-anchor">🔗</a></h2>
|
||||
@@ -353,15 +397,4 @@ sys 0m38.013s</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 "Contributor Proposals" 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>
|
||||
<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/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></channel></rss>
|
||||
]]></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></channel></rss>
|
12
README.md
12
README.md
@@ -39,12 +39,12 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <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/4560672?s=96&v=4"/></br>[mu88](https://github.com/mu88) |
|
||||
| <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) |
|
||||
| | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/2583421?s=96&v=4"/></br>[jamesandariese](https://github.com/jamesandariese) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <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/4560672?s=96&v=4"/></br>[mu88](https://github.com/mu88) | <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 -->
|
||||
|
||||
# Community
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"ignoreRegExpList": [
|
||||
"\\[.*?\\]\\(https:\\/\\/github.com\\/.*?\\)",
|
||||
"by .*?\\)",
|
||||
"\\| (.*?) \\| \\d\\d%"
|
||||
"\\| (.*?) \\| \\d+%"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/*.d.ts",
|
||||
|
@@ -14,3 +14,22 @@ module.exports = () => {
|
||||
global.console = jestConsole;
|
||||
});
|
||||
};
|
||||
|
||||
// jsdom extensions
|
||||
if (typeof document !== 'undefined') {
|
||||
// Prevents the CodeMirror error "getClientRects is undefined".
|
||||
// See https://github.com/jsdom/jsdom/issues/3002#issue-652790925
|
||||
document.createRange = () => {
|
||||
const range = new Range();
|
||||
range.getBoundingClientRect = jest.fn();
|
||||
range.getClientRects = () => {
|
||||
return {
|
||||
length: 0,
|
||||
item: () => null,
|
||||
[Symbol.iterator]: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
return range;
|
||||
};
|
||||
}
|
||||
|
@@ -186,6 +186,8 @@
|
||||
"packages/doc-builder/help": true,
|
||||
"packages/doc-builder/news": true,
|
||||
"packages/doc-builder/i18n": true,
|
||||
"packages/doc-builder/.docusaurus": true,
|
||||
"packages/doc-builder/static/images": true,
|
||||
"readme/i18n": true,
|
||||
"packages/app-cli/**/*.*~": true,
|
||||
"packages/app-cli/**/*.mo": true,
|
||||
|
@@ -107,6 +107,8 @@
|
||||
"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",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch"
|
||||
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch",
|
||||
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",
|
||||
"chokidar@^2.0.0": "3.5.3"
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ const WindowWidget = require('tkwidgets/WindowWidget.js');
|
||||
const NoteWidget = require('./gui/NoteWidget.js');
|
||||
const ResourceServer = require('./ResourceServer.js');
|
||||
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
|
||||
const FolderListWidget = require('./gui/FolderListWidget.js');
|
||||
const FolderListWidget = require('./gui/FolderListWidget').default;
|
||||
const NoteListWidget = require('./gui/NoteListWidget.js');
|
||||
const StatusBarWidget = require('./gui/StatusBarWidget').default;
|
||||
const ConsoleWidget = require('./gui/ConsoleWidget.js');
|
||||
|
@@ -1,461 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const BaseApplication_1 = require("@joplin/lib/BaseApplication");
|
||||
const folders_screen_utils_js_1 = require("@joplin/lib/folders-screen-utils.js");
|
||||
const ResourceService_1 = require("@joplin/lib/services/ResourceService");
|
||||
const BaseModel_1 = require("@joplin/lib/BaseModel");
|
||||
const Folder_1 = require("@joplin/lib/models/Folder");
|
||||
const BaseItem_1 = require("@joplin/lib/models/BaseItem");
|
||||
const Note_1 = require("@joplin/lib/models/Note");
|
||||
const Tag_1 = require("@joplin/lib/models/Tag");
|
||||
const Setting_1 = require("@joplin/lib/models/Setting");
|
||||
const registry_js_1 = require("@joplin/lib/registry.js");
|
||||
const path_utils_1 = require("@joplin/lib/path-utils");
|
||||
const utils_1 = require("@joplin/utils");
|
||||
const locale_1 = require("@joplin/lib/locale");
|
||||
const fs_extra_1 = require("fs-extra");
|
||||
const RevisionService_1 = require("@joplin/lib/services/RevisionService");
|
||||
const shim_1 = require("@joplin/lib/shim");
|
||||
const setupCommand_1 = require("./setupCommand");
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const Cache = require('@joplin/lib/Cache');
|
||||
const { splitCommandBatch } = require('@joplin/lib/string-utils');
|
||||
class Application extends BaseApplication_1.default {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.commands_ = {};
|
||||
this.commandMetadata_ = null;
|
||||
this.activeCommand_ = null;
|
||||
this.allCommandsLoaded_ = false;
|
||||
this.gui_ = null;
|
||||
this.cache_ = new Cache();
|
||||
}
|
||||
gui() {
|
||||
return this.gui_;
|
||||
}
|
||||
commandStdoutMaxWidth() {
|
||||
return this.gui().stdoutMaxWidth();
|
||||
}
|
||||
guessTypeAndLoadItem(pattern, options = null) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let type = BaseModel_1.default.TYPE_NOTE;
|
||||
if (pattern.indexOf('/') === 0) {
|
||||
type = BaseModel_1.default.TYPE_FOLDER;
|
||||
pattern = pattern.substr(1);
|
||||
}
|
||||
return this.loadItem(type, pattern, options);
|
||||
});
|
||||
}
|
||||
loadItem(type, pattern, options = null) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const output = yield this.loadItems(type, pattern, options);
|
||||
if (output.length > 1) {
|
||||
// output.sort((a, b) => { return a.user_updated_time < b.user_updated_time ? +1 : -1; });
|
||||
// let answers = { 0: _('[Cancel]') };
|
||||
// for (let i = 0; i < output.length; i++) {
|
||||
// answers[i + 1] = output[i].title;
|
||||
// }
|
||||
// Not really useful with new UI?
|
||||
throw new Error((0, locale_1._)('More than one item match "%s". Please narrow down your query.', pattern));
|
||||
// let msg = _('More than one item match "%s". Please select one:', pattern);
|
||||
// const response = await cliUtils.promptMcq(msg, answers);
|
||||
// if (!response) return null;
|
||||
// return output[response - 1];
|
||||
}
|
||||
else {
|
||||
return output.length ? output[0] : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
loadItems(type, pattern, options = null) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (type === 'folderOrNote') {
|
||||
const folders = yield this.loadItems(BaseModel_1.default.TYPE_FOLDER, pattern, options);
|
||||
if (folders.length)
|
||||
return folders;
|
||||
return yield this.loadItems(BaseModel_1.default.TYPE_NOTE, pattern, options);
|
||||
}
|
||||
pattern = pattern ? pattern.toString() : '';
|
||||
if (type === BaseModel_1.default.TYPE_FOLDER && (pattern === Folder_1.default.conflictFolderTitle() || pattern === Folder_1.default.conflictFolderId()))
|
||||
return [Folder_1.default.conflictFolder()];
|
||||
if (!options)
|
||||
options = {};
|
||||
const parent = options.parent ? options.parent : app().currentFolder();
|
||||
const ItemClass = BaseItem_1.default.itemClass(type);
|
||||
if (type === BaseModel_1.default.TYPE_NOTE && pattern.indexOf('*') >= 0) {
|
||||
// Handle it as pattern
|
||||
if (!parent)
|
||||
throw new Error((0, locale_1._)('No notebook selected.'));
|
||||
return yield Note_1.default.previews(parent.id, { titlePattern: pattern });
|
||||
}
|
||||
else {
|
||||
// Single item
|
||||
let item = null;
|
||||
if (type === BaseModel_1.default.TYPE_NOTE) {
|
||||
if (!parent)
|
||||
throw new Error((0, locale_1._)('No notebook has been specified.'));
|
||||
item = yield ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
|
||||
}
|
||||
else {
|
||||
item = yield ItemClass.loadByTitle(pattern);
|
||||
}
|
||||
if (item)
|
||||
return [item];
|
||||
item = yield ItemClass.load(pattern); // Load by id
|
||||
if (item)
|
||||
return [item];
|
||||
if (pattern.length >= 2) {
|
||||
return yield ItemClass.loadByPartialId(pattern);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
setupCommand(cmd) {
|
||||
return (0, setupCommand_1.default)(cmd, (t) => this.stdout(t), () => this.store(), () => this.gui());
|
||||
}
|
||||
stdout(text) {
|
||||
return this.gui().stdout(text);
|
||||
}
|
||||
exit(code = 0) {
|
||||
const _super = Object.create(null, {
|
||||
exit: { get: () => super.exit }
|
||||
});
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const doExit = () => __awaiter(this, void 0, void 0, function* () {
|
||||
this.gui().exit();
|
||||
yield _super.exit.call(this, code);
|
||||
});
|
||||
// Give it a few seconds to cancel otherwise exit anyway
|
||||
shim_1.default.setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
||||
yield doExit();
|
||||
}), 5000);
|
||||
if (yield registry_js_1.reg.syncTarget().syncStarted()) {
|
||||
this.stdout((0, locale_1._)('Cancelling background synchronisation... Please wait.'));
|
||||
const sync = yield registry_js_1.reg.syncTarget().synchronizer();
|
||||
yield sync.cancel();
|
||||
}
|
||||
yield doExit();
|
||||
});
|
||||
}
|
||||
commands(uiType = null) {
|
||||
if (!this.allCommandsLoaded_) {
|
||||
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
|
||||
(0, fs_extra_1.readdirSync)(__dirname).forEach(path => {
|
||||
if (path.indexOf('command-') !== 0)
|
||||
return;
|
||||
if (path.endsWith('.test.js'))
|
||||
return;
|
||||
const ext = (0, path_utils_1.fileExtension)(path);
|
||||
if (ext !== 'js')
|
||||
return;
|
||||
const CommandClass = require(`./${path}`);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled())
|
||||
return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
this.allCommandsLoaded_ = true;
|
||||
}
|
||||
if (uiType !== null) {
|
||||
const temp = {};
|
||||
for (const n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n))
|
||||
continue;
|
||||
const c = this.commands_[n];
|
||||
if (!c.supportsUi(uiType))
|
||||
continue;
|
||||
temp[n] = c;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
return this.commands_;
|
||||
}
|
||||
commandNames() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const metadata = yield this.commandMetadata();
|
||||
const output = [];
|
||||
for (const n in metadata) {
|
||||
if (!metadata.hasOwnProperty(n))
|
||||
continue;
|
||||
output.push(n);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
commandMetadata() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (this.commandMetadata_)
|
||||
return this.commandMetadata_;
|
||||
let output = yield this.cache_.getItem('metadata');
|
||||
if (output) {
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
}
|
||||
const commands = this.commands();
|
||||
output = {};
|
||||
for (const n in commands) {
|
||||
if (!commands.hasOwnProperty(n))
|
||||
continue;
|
||||
const cmd = commands[n];
|
||||
output[n] = cmd.metadata();
|
||||
}
|
||||
yield this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
});
|
||||
}
|
||||
hasGui() {
|
||||
return this.gui() && !this.gui().isDummy();
|
||||
}
|
||||
findCommandByName(name) {
|
||||
if (this.commands_[name])
|
||||
return this.commands_[name];
|
||||
let CommandClass = null;
|
||||
try {
|
||||
CommandClass = require(`${__dirname}/command-${name}.js`);
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
const e = new Error((0, locale_1._)('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
}
|
||||
else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
let cmd = new CommandClass();
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[name] = cmd;
|
||||
return this.commands_[name];
|
||||
}
|
||||
dummyGui() {
|
||||
return {
|
||||
isDummy: () => {
|
||||
return true;
|
||||
},
|
||||
prompt: (initialText = '', promptString = '', options = null) => {
|
||||
return cliUtils.prompt(initialText, promptString, options);
|
||||
},
|
||||
showConsole: () => { },
|
||||
maximizeConsole: () => { },
|
||||
stdout: (text) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(text);
|
||||
},
|
||||
fullScreen: () => { },
|
||||
exit: () => { },
|
||||
showModalOverlay: () => { },
|
||||
hideModalOverlay: () => { },
|
||||
stdoutMaxWidth: () => {
|
||||
return 100;
|
||||
},
|
||||
forceRender: () => { },
|
||||
termSaveState: () => { },
|
||||
termRestoreState: () => { },
|
||||
};
|
||||
}
|
||||
execCommand(argv) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (!argv.length)
|
||||
return this.execCommand(['help']);
|
||||
// reg.logger().debug('execCommand()', argv);
|
||||
const commandName = argv[0];
|
||||
this.activeCommand_ = this.findCommandByName(commandName);
|
||||
let outException = null;
|
||||
try {
|
||||
if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli'))
|
||||
throw new Error((0, locale_1._)('The command "%s" is only available in GUI mode', this.activeCommand_.name()));
|
||||
const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv);
|
||||
yield this.activeCommand_.action(cmdArgs);
|
||||
}
|
||||
catch (error) {
|
||||
outException = error;
|
||||
}
|
||||
this.activeCommand_ = null;
|
||||
if (outException)
|
||||
throw outException;
|
||||
});
|
||||
}
|
||||
currentCommand() {
|
||||
return this.activeCommand_;
|
||||
}
|
||||
loadKeymaps() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const defaultKeyMap = [
|
||||
{ keys: [':'], type: 'function', command: 'enter_command_line_mode' },
|
||||
{ keys: ['TAB'], type: 'function', command: 'focus_next' },
|
||||
{ keys: ['SHIFT_TAB'], type: 'function', command: 'focus_previous' },
|
||||
{ keys: ['UP'], type: 'function', command: 'move_up' },
|
||||
{ keys: ['DOWN'], type: 'function', command: 'move_down' },
|
||||
{ keys: ['PAGE_UP'], type: 'function', command: 'page_up' },
|
||||
{ keys: ['PAGE_DOWN'], type: 'function', command: 'page_down' },
|
||||
{ keys: ['ENTER'], type: 'function', command: 'activate' },
|
||||
{ keys: ['DELETE', 'BACKSPACE'], type: 'function', command: 'delete' },
|
||||
{ keys: ['n'], type: 'function', command: 'next_link' },
|
||||
{ keys: ['b'], type: 'function', command: 'previous_link' },
|
||||
{ keys: ['o'], type: 'function', command: 'open_link' },
|
||||
{ keys: [' '], type: 'prompt', command: 'todo toggle $n' },
|
||||
{ keys: ['tc'], type: 'function', command: 'toggle_console' },
|
||||
{ keys: ['tm'], type: 'function', command: 'toggle_metadata' },
|
||||
{ keys: ['ti'], type: 'function', command: 'toggle_ids' },
|
||||
{ keys: ['/'], type: 'prompt', command: 'search ""', cursorPosition: -2 },
|
||||
{ keys: ['mn'], type: 'prompt', command: 'mknote ""', cursorPosition: -2 },
|
||||
{ keys: ['mt'], type: 'prompt', command: 'mktodo ""', cursorPosition: -2 },
|
||||
{ keys: ['mb'], type: 'prompt', command: 'mkbook ""', cursorPosition: -2 },
|
||||
{ keys: ['yn'], type: 'prompt', command: 'cp $n ""', cursorPosition: -2 },
|
||||
{ keys: ['dn'], type: 'prompt', command: 'mv $n ""', cursorPosition: -2 },
|
||||
];
|
||||
// Filter the keymap item by command so that items in keymap.json can override
|
||||
// the default ones.
|
||||
const itemsByCommand = {};
|
||||
for (let i = 0; i < defaultKeyMap.length; i++) {
|
||||
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i];
|
||||
}
|
||||
const filePath = `${Setting_1.default.value('profileDir')}/keymap.json`;
|
||||
if (yield (0, fs_extra_1.pathExists)(filePath)) {
|
||||
try {
|
||||
let configString = yield (0, fs_extra_1.readFile)(filePath, 'utf-8');
|
||||
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
|
||||
const keymap = JSON.parse(configString);
|
||||
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
|
||||
const item = keymap[keymapIndex];
|
||||
itemsByCommand[item.command] = item;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = `Could not load keymap ${filePath}\n${msg}`;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const output = [];
|
||||
for (const n in itemsByCommand) {
|
||||
if (!itemsByCommand.hasOwnProperty(n))
|
||||
continue;
|
||||
output.push(itemsByCommand[n]);
|
||||
}
|
||||
// Map reserved shortcuts to their equivalent key
|
||||
// https://github.com/cronvel/terminal-kit/issues/101
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
const newKeys = output[i].keys.map(k => {
|
||||
k = k.replace(/CTRL_H/g, 'BACKSPACE');
|
||||
k = k.replace(/CTRL_I/g, 'TAB');
|
||||
k = k.replace(/CTRL_M/g, 'ENTER');
|
||||
return k;
|
||||
});
|
||||
output[i].keys = newKeys;
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
commandList(argv) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (argv.length && argv[0] === 'batch') {
|
||||
const commands = [];
|
||||
const commandLines = splitCommandBatch(yield (0, fs_extra_1.readFile)(argv[1], 'utf-8'));
|
||||
for (const commandLine of commandLines) {
|
||||
if (!commandLine.trim())
|
||||
continue;
|
||||
const splitted = (0, utils_1.splitCommandString)(commandLine.trim());
|
||||
commands.push(splitted);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
else {
|
||||
return [argv];
|
||||
}
|
||||
});
|
||||
}
|
||||
// We need this special case here because by the time the `version` command
|
||||
// runs, the keychain has already been setup.
|
||||
checkIfKeychainEnabled(argv) {
|
||||
return argv.indexOf('version') < 0;
|
||||
}
|
||||
start(argv) {
|
||||
const _super = Object.create(null, {
|
||||
start: { get: () => super.start }
|
||||
});
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const keychainEnabled = this.checkIfKeychainEnabled(argv);
|
||||
argv = yield _super.start.call(this, argv, { keychainEnabled });
|
||||
cliUtils.setStdout((object) => {
|
||||
return this.stdout(object);
|
||||
});
|
||||
this.initRedux();
|
||||
// If we have some arguments left at this point, it's a command
|
||||
// so execute it.
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
this.currentFolder_ = yield Folder_1.default.load(Setting_1.default.value('activeFolderId'));
|
||||
yield this.applySettingsSideEffects();
|
||||
try {
|
||||
const commands = yield this.commandList(argv);
|
||||
for (const command of commands) {
|
||||
yield this.execCommand(command);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.error(error);
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
yield Setting_1.default.saveAll();
|
||||
// Need to call exit() explicitly, otherwise Node wait for any timeout to complete
|
||||
// https://stackoverflow.com/questions/18050095
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
// Otherwise open the GUI
|
||||
const keymap = yield this.loadKeymaps();
|
||||
const AppGui = require('./app-gui.js');
|
||||
this.gui_ = new AppGui(this, this.store(), keymap);
|
||||
this.gui_.setLogger(this.logger());
|
||||
yield this.gui_.start();
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting_1.default.dispatchUpdateAll();
|
||||
yield (0, folders_screen_utils_js_1.refreshFolders)((action) => this.store().dispatch(action));
|
||||
const tags = yield Tag_1.default.allWithNotes();
|
||||
ResourceService_1.default.runInBackground();
|
||||
RevisionService_1.default.instance().runInBackground();
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
items: tags,
|
||||
});
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: Setting_1.default.value('activeFolderId'),
|
||||
});
|
||||
this.startRotatingLogMaintenance(Setting_1.default.value('profileDir'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let application_ = null;
|
||||
function app() {
|
||||
if (application_)
|
||||
return application_;
|
||||
application_ = new Application();
|
||||
return application_;
|
||||
}
|
||||
exports.default = app;
|
||||
//# sourceMappingURL=app.js.map
|
@@ -95,7 +95,7 @@ class Application extends BaseApplication {
|
||||
let item = null;
|
||||
if (type === BaseModel.TYPE_NOTE) {
|
||||
if (!parent) throw new Error(_('No notebook has been specified.'));
|
||||
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
|
||||
item = await (ItemClass as typeof Note).loadFolderNoteByField(parent.id, 'title', pattern);
|
||||
} else {
|
||||
item = await ItemClass.loadByTitle(pattern);
|
||||
}
|
||||
@@ -307,6 +307,7 @@ class Application extends BaseApplication {
|
||||
{ keys: ['tc'], type: 'function', command: 'toggle_console' },
|
||||
{ keys: ['tm'], type: 'function', command: 'toggle_metadata' },
|
||||
{ keys: ['ti'], type: 'function', command: 'toggle_ids' },
|
||||
{ keys: ['r'], type: 'prompt', command: 'restore $n' },
|
||||
{ keys: ['/'], type: 'prompt', command: 'search ""', cursorPosition: -2 },
|
||||
{ keys: ['mn'], type: 'prompt', command: 'mknote ""', cursorPosition: -2 },
|
||||
{ keys: ['mt'], type: 'prompt', command: 'mktodo ""', cursorPosition: -2 },
|
||||
|
@@ -400,6 +400,11 @@ async function fetchAllNotes() {
|
||||
lines.push('Remove the tag from the note.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_NOTE || model.type === BaseModel.TYPE_FOLDER) {
|
||||
lines.push(`By default, the ${singular} will be moved **to the trash**. To permanently delete it, add the query parameter \`permanent=1\``);
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
@@ -2,6 +2,7 @@ import BaseCommand from './base-command';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -17,7 +18,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
public override async action() {
|
||||
let items = [];
|
||||
let items: (NoteEntity | FolderEntity)[] = [];
|
||||
const folders = await Folder.all();
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
|
@@ -7,6 +7,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
import time from '@joplin/lib/time';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -71,7 +72,7 @@ class Command extends BaseCommand {
|
||||
let hasTodos = false;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.is_todo) {
|
||||
if ((item as NoteEntity).is_todo) {
|
||||
hasTodos = true;
|
||||
break;
|
||||
}
|
||||
@@ -103,8 +104,8 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
if (hasTodos) {
|
||||
if (item.is_todo) {
|
||||
row.push(sprintf('[%s]', item.todo_completed ? 'X' : ' '));
|
||||
if ((item as NoteEntity).is_todo) {
|
||||
row.push(sprintf('[%s]', (item as NoteEntity).todo_completed ? 'X' : ' '));
|
||||
} else {
|
||||
row.push(' ');
|
||||
}
|
||||
|
26
packages/app-cli/app/command-restore.ts
Normal file
26
packages/app-cli/app/command-restore.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
return 'restore <pattern>';
|
||||
}
|
||||
|
||||
public override description() {
|
||||
return _('Restore the items matching <pattern> from the trash.');
|
||||
}
|
||||
|
||||
public override async action(args: any) {
|
||||
const pattern = args['pattern'];
|
||||
|
||||
const items = await app().loadItems('folderOrNote', pattern);
|
||||
if (!items.length) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
const ids = items.map(n => n.id);
|
||||
await restoreItems(items[0].type_, ids, { useRestoreFolder: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
81
packages/app-cli/app/command-rmbook.test.ts
Normal file
81
packages/app-cli/app/command-rmbook.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const Command = require('./command-rmbook');
|
||||
|
||||
const setUpCommand = () => {
|
||||
const command = setupCommandForTesting(Command);
|
||||
const promptMock = jest.fn(() => true);
|
||||
command.setPrompt(promptMock);
|
||||
|
||||
return { command, promptMock };
|
||||
};
|
||||
|
||||
|
||||
describe('command-rmbook', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
await setupApplication();
|
||||
});
|
||||
|
||||
it('should ask before moving to the trash', async () => {
|
||||
await Folder.save({ title: 'folder1' });
|
||||
|
||||
const { command, promptMock } = setUpCommand();
|
||||
|
||||
await command.action({ 'notebook': 'folder1', options: {} });
|
||||
|
||||
expect(promptMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const folder1 = await Folder.loadByTitle('folder1');
|
||||
expect(folder1.deleted_time).not.toBeFalsy();
|
||||
expect((await Note.allItemsInTrash()).folderIds).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('cancelling a prompt should prevent deletion', async () => {
|
||||
await Folder.save({ title: 'folder1' });
|
||||
|
||||
const { command, promptMock } = setUpCommand();
|
||||
promptMock.mockImplementation(() => false);
|
||||
await command.action({ 'notebook': 'folder1', options: {} });
|
||||
|
||||
expect((await Note.allItemsInTrash()).folderIds).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not prompt when the force flag is given', async () => {
|
||||
const { id: folder1Id } = await Folder.save({ title: 'folder1' });
|
||||
const { id: folder2Id } = await Folder.save({ title: 'folder2', parent_id: folder1Id });
|
||||
|
||||
const { command, promptMock } = setUpCommand();
|
||||
await command.action({ 'notebook': 'folder1', options: { force: true } });
|
||||
|
||||
expect(promptMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect((await Note.allItemsInTrash()).folderIds.includes(folder1Id)).toBe(true);
|
||||
expect((await Note.allItemsInTrash()).folderIds.includes(folder2Id)).toBe(true);
|
||||
});
|
||||
|
||||
it('should support permanent deletion', async () => {
|
||||
const { id: folder1Id } = await Folder.save({ title: 'folder1' });
|
||||
const { id: folder2Id } = await Folder.save({ title: 'folder2' });
|
||||
|
||||
const { command, promptMock } = setUpCommand();
|
||||
await command.action({ 'notebook': 'folder1', options: { permanent: true, force: true } });
|
||||
expect(promptMock).not.toHaveBeenCalled();
|
||||
|
||||
// Should be permanently deleted.
|
||||
expect((await Note.allItemsInTrash()).folderIds.includes(folder1Id)).toBe(false);
|
||||
expect(await Folder.load(folder1Id, { includeDeleted: true })).toBe(undefined);
|
||||
|
||||
// folder2 should not be deleted
|
||||
expect(await Folder.load(folder2Id, { includeDeleted: false })).toBeTruthy();
|
||||
|
||||
// Should prompt before deleting
|
||||
await command.action({ 'notebook': 'folder2', options: { permanent: true } });
|
||||
expect(promptMock).toHaveBeenCalled();
|
||||
expect(await Folder.load(folder2Id, { includeDeleted: false })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@@ -3,6 +3,7 @@ import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -14,7 +15,10 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
public override options() {
|
||||
return [['-f, --force', _('Deletes the notebook without asking for confirmation.')]];
|
||||
return [
|
||||
['-f, --force', _('Deletes the notebook without asking for confirmation.')],
|
||||
['-p, --permanent', _('Permanently deletes the notebook, skipping the trash.')],
|
||||
];
|
||||
}
|
||||
|
||||
public override async action(args: any) {
|
||||
@@ -23,10 +27,19 @@ class Command extends BaseCommand {
|
||||
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
|
||||
|
||||
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
||||
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
||||
let msg;
|
||||
if (permanent) {
|
||||
msg = _('Permanently delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will be permanently deleted.', ellipsizedFolderTitle);
|
||||
} else {
|
||||
msg = _('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', ellipsizedFolderTitle);
|
||||
}
|
||||
const ok = force ? true : await this.prompt(msg, { booleanAnswerDefault: 'n' });
|
||||
if (!ok) return;
|
||||
|
||||
await Folder.delete(folder.id);
|
||||
await Folder.delete(folder.id, { toTrash: !permanent, sourceDescription: 'rmbook command' });
|
||||
}
|
||||
}
|
||||
|
||||
|
57
packages/app-cli/app/command-rmnote.test.ts
Normal file
57
packages/app-cli/app/command-rmnote.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import app from './app';
|
||||
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
const Command = require('./command-rmnote');
|
||||
|
||||
const setUpCommand = () => {
|
||||
const command = setupCommandForTesting(Command);
|
||||
const promptMock = jest.fn(() => true);
|
||||
command.setPrompt(promptMock);
|
||||
|
||||
return { command, promptMock };
|
||||
};
|
||||
|
||||
const createTestNote = async () => {
|
||||
const folder = await Folder.save({ title: 'folder' });
|
||||
app().switchCurrentFolder(folder);
|
||||
return await Note.save({ title: 'note1', parent_id: folder.id });
|
||||
};
|
||||
|
||||
|
||||
describe('command-rmnote', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
await setupApplication();
|
||||
});
|
||||
|
||||
it('should move to the trash by default, without prompting', async () => {
|
||||
const { id: noteId } = await createTestNote();
|
||||
|
||||
const { command, promptMock } = setUpCommand();
|
||||
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||
expect(promptMock).not.toHaveBeenCalled();
|
||||
|
||||
expect((await Note.allItemsInTrash()).noteIds.includes(noteId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should permanently delete trashed items by default, with prompting', async () => {
|
||||
const { id: noteId } = await createTestNote();
|
||||
const { command, promptMock } = setUpCommand();
|
||||
|
||||
// Should not prompt when deleting from a folder
|
||||
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||
expect(promptMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Should prompt when deleting from trash
|
||||
app().switchCurrentFolder(await Folder.load(getTrashFolderId()));
|
||||
await command.action({ 'note-pattern': 'note1', options: {} });
|
||||
expect(promptMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(await Note.load(noteId, { includeDeleted: true })).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
@@ -2,7 +2,8 @@ import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -14,20 +15,40 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
public override options() {
|
||||
return [['-f, --force', _('Deletes the notes without asking for confirmation.')]];
|
||||
return [
|
||||
['-f, --force', _('Deletes the notes without asking for confirmation.')],
|
||||
['-p, --permanent', _('Deletes notes permanently, skipping the trash.')],
|
||||
];
|
||||
}
|
||||
|
||||
public override async action(args: any) {
|
||||
const pattern = args['note-pattern'];
|
||||
const force = args.options && args.options.force === true;
|
||||
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
const notes: NoteEntity[] = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
const ok = force ? true : await this.prompt(notes.length > 1 ? _('%d notes match this pattern. Delete them?', notes.length) : _('Delete note?'), { booleanAnswerDefault: 'n' });
|
||||
let ok = true;
|
||||
if (!force && notes.length > 1) {
|
||||
ok = await this.prompt(_('%d notes match this pattern. Delete them?', notes.length), { booleanAnswerDefault: 'n' });
|
||||
}
|
||||
|
||||
const permanent = (args.options?.permanent === true) || notes.every(n => !!n.deleted_time);
|
||||
if (!force && permanent) {
|
||||
const message = (
|
||||
notes.length === 1 ? _('This will permanently delete the note "%s". Continue?', notes[0].title) : _('%d notes will be permanently deleted. Continue?', notes.length)
|
||||
);
|
||||
ok = await this.prompt(message, { booleanAnswerDefault: 'n' });
|
||||
}
|
||||
|
||||
if (!ok) return;
|
||||
const ids = notes.map((n: any) => n.id);
|
||||
await Note.batchDelete(ids);
|
||||
|
||||
const ids = notes.map(n => n.id);
|
||||
const options: DeleteOptions = {
|
||||
toTrash: !permanent,
|
||||
sourceDescription: 'rmnote',
|
||||
};
|
||||
await Note.batchDelete(ids, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,11 @@ const { cliUtils } = require('./cli-utils.js');
|
||||
const md5 = require('md5');
|
||||
import * as locker from 'proper-lockfile';
|
||||
import { pathExists, writeFile } from 'fs-extra';
|
||||
import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
|
||||
const logger = Logger.create('command-sync');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -84,6 +89,33 @@ class Command extends BaseCommand {
|
||||
Setting.setValue(`sync.${this.syncTargetId_}.auth`, response.access_token);
|
||||
api.setAuthToken(response.access_token);
|
||||
return true;
|
||||
} else if (syncTargetMd.name === 'joplinCloud') {
|
||||
const applicationAuthId = uuidgen();
|
||||
const checkForCredentials = async () => {
|
||||
try {
|
||||
const applicationAuthUrl = `${Setting.value('sync.10.path')}/api/application_auth/${applicationAuthId}`;
|
||||
const response = await checkIfLoginWasSuccessful(applicationAuthUrl);
|
||||
if (response && response.success) {
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
this.stdout(_('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:'));
|
||||
|
||||
const confirmUrl = `${Setting.value('sync.10.website')}/applications/${applicationAuthId}/confirm`;
|
||||
const urlWithClient = await generateApplicationConfirmUrl(confirmUrl);
|
||||
this.stdout(urlWithClient);
|
||||
|
||||
const authorized = await this.prompt(_('Have you authorised the application login in the above URL?'), { booleanAnswerDefault: 'y' });
|
||||
if (!authorized) return false;
|
||||
const result = await checkForCredentials();
|
||||
if (!result) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stdout(_('Not authenticated with %s. Please provide any missing credentials.', syncTargetMd.label));
|
||||
|
@@ -85,7 +85,7 @@ class Command extends BaseCommand {
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const noteId = randomElement(noteIds);
|
||||
promises.push(Note.delete(noteId));
|
||||
promises.push(Note.delete(noteId, { sourceDescription: 'command-testing' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,20 @@
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const Tag = require('@joplin/lib/models/Tag').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { getDisplayParentId, getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const _ = require('@joplin/lib/locale')._;
|
||||
|
||||
class FolderListWidget extends ListWidget {
|
||||
constructor() {
|
||||
export default class FolderListWidget extends ListWidget {
|
||||
|
||||
private folders_: FolderEntity[] = [];
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.tags_ = [];
|
||||
this.folders_ = [];
|
||||
this.searches_ = [];
|
||||
this.selectedFolderId_ = null;
|
||||
this.selectedTagId_ = null;
|
||||
@@ -21,7 +25,7 @@ class FolderListWidget extends ListWidget {
|
||||
this.trimItemTitle = false;
|
||||
this.showIds = false;
|
||||
|
||||
this.itemRenderer = item => {
|
||||
this.itemRenderer = (item: any) => {
|
||||
const output = [];
|
||||
if (item === '-') {
|
||||
output.push('-'.repeat(this.innerWidth));
|
||||
@@ -33,13 +37,12 @@ class FolderListWidget extends ListWidget {
|
||||
}
|
||||
output.push(Folder.displayTitle(item));
|
||||
|
||||
if (Setting.value('showNoteCounts')) {
|
||||
if (Setting.value('showNoteCounts') && !item.deleted_time && item.id !== getTrashFolderId()) {
|
||||
let noteCount = item.note_count;
|
||||
// Subtract children note_count from parent folder.
|
||||
if (this.folderHasChildren_(this.folders, item.id)) {
|
||||
for (let i = 0; i < this.folders.length; i++) {
|
||||
if (this.folders[i].parent_id === item.id) {
|
||||
noteCount -= this.folders[i].note_count;
|
||||
noteCount -= (this.folders[i] as any).note_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,113 +59,121 @@ class FolderListWidget extends ListWidget {
|
||||
};
|
||||
}
|
||||
|
||||
folderDepth(folders, folderId) {
|
||||
public folderDepth(folders: FolderEntity[], folderId: string) {
|
||||
let output = 0;
|
||||
while (true) {
|
||||
const folder = BaseModel.byId(folders, folderId);
|
||||
if (!folder || !folder.parent_id) return output;
|
||||
const folderParentId = getDisplayParentId(folder, folders.find(f => f.id === folder.parent_id));
|
||||
if (!folder || !folderParentId) return output;
|
||||
output++;
|
||||
folderId = folder.parent_id;
|
||||
folderId = folderParentId;
|
||||
}
|
||||
}
|
||||
|
||||
get selectedFolderId() {
|
||||
public get selectedFolderId() {
|
||||
return this.selectedFolderId_;
|
||||
}
|
||||
|
||||
set selectedFolderId(v) {
|
||||
public set selectedFolderId(v) {
|
||||
this.selectedFolderId_ = v;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get selectedSearchId() {
|
||||
public get selectedSearchId() {
|
||||
return this.selectedSearchId_;
|
||||
}
|
||||
|
||||
set selectedSearchId(v) {
|
||||
public set selectedSearchId(v) {
|
||||
this.selectedSearchId_ = v;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get selectedTagId() {
|
||||
public get selectedTagId() {
|
||||
return this.selectedTagId_;
|
||||
}
|
||||
|
||||
set selectedTagId(v) {
|
||||
public set selectedTagId(v) {
|
||||
this.selectedTagId_ = v;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get notesParentType() {
|
||||
public get notesParentType() {
|
||||
return this.notesParentType_;
|
||||
}
|
||||
|
||||
set notesParentType(v) {
|
||||
public set notesParentType(v) {
|
||||
this.notesParentType_ = v;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get searches() {
|
||||
public get searches() {
|
||||
return this.searches_;
|
||||
}
|
||||
|
||||
set searches(v) {
|
||||
public set searches(v) {
|
||||
this.searches_ = v;
|
||||
this.updateItems_ = true;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get tags() {
|
||||
public get tags() {
|
||||
return this.tags_;
|
||||
}
|
||||
|
||||
set tags(v) {
|
||||
public set tags(v) {
|
||||
this.tags_ = v;
|
||||
this.updateItems_ = true;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
get folders() {
|
||||
public get folders() {
|
||||
return this.folders_;
|
||||
}
|
||||
|
||||
set folders(v) {
|
||||
public set folders(v) {
|
||||
this.folders_ = v;
|
||||
this.updateItems_ = true;
|
||||
this.updateIndexFromSelectedItemId();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
toggleShowIds() {
|
||||
public toggleShowIds() {
|
||||
this.showIds = !this.showIds;
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
folderHasChildren_(folders, folderId) {
|
||||
public folderHasChildren_(folders: FolderEntity[], folderId: string) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
if (folder.parent_id === folderId) return true;
|
||||
const folderParentId = getDisplayParentId(folder, folders.find(f => f.id === folder.parent_id));
|
||||
if (folderParentId === folderId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (this.updateItems_) {
|
||||
this.logger().debug('Rebuilding items...', this.notesParentType, this.selectedJoplinItemId, this.selectedSearchId);
|
||||
const wasSelectedItemId = this.selectedJoplinItemId;
|
||||
const previousParentType = this.notesParentType;
|
||||
|
||||
let newItems = [];
|
||||
const orderFolders = parentId => {
|
||||
this.logger().info('FFFFFFFFFFFFF', JSON.stringify(this.folders, null, 4));
|
||||
|
||||
let newItems: any[] = [];
|
||||
const orderFolders = (parentId: string) => {
|
||||
this.logger().info('PARENT', parentId);
|
||||
for (let i = 0; i < this.folders.length; i++) {
|
||||
const f = this.folders[i];
|
||||
const folderParentId = f.parent_id ? f.parent_id : '';
|
||||
const originalParent = this.folders_.find(f => f.id === f.parent_id);
|
||||
|
||||
const folderParentId = getDisplayParentId(f, originalParent); // f.parent_id ? f.parent_id : '';
|
||||
this.logger().info('FFF', f.title, folderParentId);
|
||||
if (folderParentId === parentId) {
|
||||
newItems.push(f);
|
||||
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
|
||||
@@ -192,7 +203,7 @@ class FolderListWidget extends ListWidget {
|
||||
super.render();
|
||||
}
|
||||
|
||||
get selectedJoplinItemId() {
|
||||
public get selectedJoplinItemId() {
|
||||
if (!this.notesParentType) return '';
|
||||
if (this.notesParentType === 'Folder') return this.selectedFolderId;
|
||||
if (this.notesParentType === 'Tag') return this.selectedTagId;
|
||||
@@ -200,17 +211,15 @@ class FolderListWidget extends ListWidget {
|
||||
throw new Error(`Unknown parent type: ${this.notesParentType}`);
|
||||
}
|
||||
|
||||
get selectedJoplinItem() {
|
||||
public get selectedJoplinItem() {
|
||||
const id = this.selectedJoplinItemId;
|
||||
const index = this.itemIndexByKey('id', id);
|
||||
return this.itemAt(index);
|
||||
}
|
||||
|
||||
updateIndexFromSelectedItemId(itemId = null) {
|
||||
public updateIndexFromSelectedItemId(itemId: string = null) {
|
||||
if (itemId === null) itemId = this.selectedJoplinItemId;
|
||||
const index = this.itemIndexByKey('id', itemId);
|
||||
this.currentIndex = index >= 0 ? index : 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FolderListWidget;
|
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.14.0",
|
||||
"version": "3.0.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~2.14",
|
||||
"@joplin/renderer": "~2.14",
|
||||
"@joplin/utils": "~2.14",
|
||||
"@joplin/lib": "~3.0",
|
||||
"@joplin/renderer": "~3.0",
|
||||
"@joplin/utils": "~3.0",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -70,7 +70,7 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.14",
|
||||
"@joplin/tools": "~3.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.8",
|
||||
|
@@ -7,9 +7,10 @@
|
||||
<updated>20231224T151443Z</updated>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/" rel="noopener noreferrer" rev="en_rl_none">Note 2</a><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/" rel="noopener noreferrer" rev="en_rl_none">Note 3</a></div></en-note> ]]>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/">Note 2</a><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/">Note 3</a></div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Note 2</title>
|
||||
<created>20160730T111759Z</created>
|
||||
@@ -19,15 +20,37 @@
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div>Testing</div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Note 3</title>
|
||||
<created>20160730T111759Z</created>
|
||||
<updated>20160730T111807Z</updated>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/" rel="noopener noreferrer" rev="en_rl_none">Ambiguous note</a></div></en-note> ]]>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><a href="evernote:///view/5223870/s49/9cd5e810-fa03-429a-8194-ab847f2f1ab2/c99d9e01-ca35-4c75-ba63-f0c0ef97787d/">Ambiguous note</a></div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Note 4</title>
|
||||
<created>20160730T111759Z</created>
|
||||
<updated>20160730T111807Z</updated>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><a href="https://joplinapp.org">Note 5</a></div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Note 5</title>
|
||||
<created>20160730T111759Z</created>
|
||||
<updated>20160730T111807Z</updated>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div></div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Ambiguous note</title>
|
||||
<created>20160730T111759Z</created>
|
||||
@@ -37,6 +60,7 @@
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div>Testing</div></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
|
||||
<note>
|
||||
<title>Ambiguous note</title>
|
||||
<created>20160730T111759Z</created>
|
||||
|
@@ -566,8 +566,8 @@ export interface CodeMirrorControl {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeMirrorContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
|
||||
plugin: (codeMirrorControl: CodeMirrorControl)=> void;
|
||||
export interface MarkdownEditorContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
|
||||
plugin: (editorControl: CodeMirrorControl)=> void;
|
||||
}
|
||||
|
||||
export enum ContentScriptType {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
//
|
||||
import { lineNumbers, highlightActiveLineGutter, EditorView } from '@codemirror/view';
|
||||
import { completeFromList } from '@codemirror/autocomplete';
|
||||
import { CodeMirrorContentScriptModule, ContentScriptContext } from 'api/types';
|
||||
import { MarkdownEditorContentScriptModule, ContentScriptContext } from 'api/types';
|
||||
//
|
||||
// For the above import to work, you may also need to add @codemirror/view as a dev dependency
|
||||
// to package.json. (For the type information only).
|
||||
@@ -15,7 +15,7 @@ import { CodeMirrorContentScriptModule, ContentScriptContext } from 'api/types';
|
||||
// const { lineNumbers } = joplin.require('@codemirror/view');
|
||||
|
||||
|
||||
export default (_context: ContentScriptContext): CodeMirrorContentScriptModule => {
|
||||
export default (_context: ContentScriptContext): MarkdownEditorContentScriptModule => {
|
||||
return {
|
||||
// - codeMirrorWrapper: A thin wrapper around CodeMirror 6, designed to be similar to the
|
||||
// CodeMirror 5 API. If running in CodeMirror 5, a CodeMirror object is provided instead.
|
||||
|
@@ -3,7 +3,7 @@ const leftPad = require('left-pad');
|
||||
export default function(context) {
|
||||
return {
|
||||
plugin: function(markdownIt, _options) {
|
||||
const pluginId = context.pluginId;
|
||||
const contentScriptId = context.contentScriptId;
|
||||
|
||||
const defaultRender = markdownIt.renderer.rules.fence || function(tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
@@ -14,15 +14,30 @@ export default function(context) {
|
||||
if (token.info !== 'justtesting') return defaultRender(tokens, idx, options, env, self);
|
||||
|
||||
const postMessageWithResponseTest = `
|
||||
webviewApi.postMessage('${pluginId}', 'justtesting').then(function(response) {
|
||||
webviewApi.postMessage('${contentScriptId}', 'justtesting').then(function(response) {
|
||||
console.info('Got response in content script: ' + response);
|
||||
});
|
||||
return false;
|
||||
`;
|
||||
|
||||
// Rich text editor support:
|
||||
// The joplin-editable and joplin-source CSS classes mark the generated div
|
||||
// as a region that needs special processing when converting back to markdown.
|
||||
// This element helps Joplin reconstruct the original markdown.
|
||||
const richTextEditorMetadata = `
|
||||
<pre
|
||||
class="joplin-source"
|
||||
data-joplin-language="justtesting"
|
||||
data-joplin-source-open="\`\`\`justtesting\n"
|
||||
data-joplin-source-close="\`\`\`"
|
||||
>${markdownIt.utils.escapeHtml(token.content)}</pre>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="just-testing">
|
||||
<p>JUST TESTING: <pre>${leftPad(token.content.trim(), 10, 'x')}</pre></p>
|
||||
<div class="just-testing joplin-editable">
|
||||
${richTextEditorMetadata}
|
||||
|
||||
<p>JUST TESTING: <pre>${markdownIt.utils.escapeHtml(leftPad(token.content.trim(), 10, 'x'))}</pre></p>
|
||||
<p><a href="#" onclick="${postMessageWithResponseTest.replace(/\n/g, ' ')}">Click to post a message "justtesting" to plugin and check the response in the console</a></p>
|
||||
</div>
|
||||
`;
|
||||
|
@@ -22,7 +22,7 @@ const registerSimpleTopToBottomRenderer = async () => {
|
||||
|
||||
dependencies: [
|
||||
'item.selected',
|
||||
'note.titleHtml',
|
||||
'note.title',
|
||||
'note.body',
|
||||
'note.user_updated_time',
|
||||
],
|
||||
@@ -55,8 +55,8 @@ const registerSimpleTopToBottomRenderer = async () => {
|
||||
itemTemplate: // html
|
||||
`
|
||||
<div class="content {{#item.selected}}-selected{{/item.selected}}">
|
||||
<p class="title">{{{note.titleHtml}}}</p>
|
||||
<p class="date">{{{updatedTime}}}</p>
|
||||
<p class="title">{{note.title}}</p>
|
||||
<p class="date">{{updatedTime}}</p>
|
||||
<p class="body">{{noteBody}}</p>
|
||||
</div>
|
||||
`,
|
||||
@@ -90,7 +90,7 @@ const registerSimpleLeftToRightRenderer = async() => {
|
||||
dependencies: [
|
||||
'note.id',
|
||||
'item.selected',
|
||||
'note.titleHtml',
|
||||
'note.title',
|
||||
'note.body',
|
||||
],
|
||||
|
||||
@@ -124,7 +124,7 @@ const registerSimpleLeftToRightRenderer = async() => {
|
||||
<img class="thumbnail" src="file://{{thumbnailFilePath}}"/>
|
||||
{{/thumbnailFilePath}}
|
||||
{{^thumbnailFilePath}}
|
||||
{{{note.titleHtml}}}
|
||||
{{{note.title}}}
|
||||
{{/thumbnailFilePath}}
|
||||
</div>
|
||||
`,
|
||||
|
@@ -11,13 +11,9 @@
|
||||
if (typeof browser !== 'undefined') {
|
||||
// eslint-disable-next-line no-undef
|
||||
browser_ = browser;
|
||||
// eslint-disable-next-line no-undef
|
||||
browserSupportsPromises_ = true;
|
||||
} else if (typeof chrome !== 'undefined') {
|
||||
// eslint-disable-next-line no-undef
|
||||
browser_ = chrome;
|
||||
// eslint-disable-next-line no-undef
|
||||
browserSupportsPromises_ = false;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
@@ -461,6 +457,7 @@
|
||||
tags: command.tags,
|
||||
windowInnerWidth: window.innerWidth,
|
||||
windowInnerHeight: window.innerHeight,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
};
|
||||
|
||||
browser_.runtime.sendMessage({
|
||||
|
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.14.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||
},
|
||||
"icons": {
|
||||
"32": "icons/32.png",
|
||||
"48": "icons/48.png",
|
||||
@@ -13,18 +15,21 @@
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"<all_urls>",
|
||||
"scripting",
|
||||
"storage"
|
||||
],
|
||||
"browser_action": {
|
||||
"host_permissions": [
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_icon": "icons/32.png",
|
||||
"default_title": "Joplin Web Clipper",
|
||||
"default_popup": "popup/build/index.html"
|
||||
},
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"_execute_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Shift+J"
|
||||
}
|
||||
@@ -49,12 +54,12 @@
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background.js"
|
||||
],
|
||||
"persistent": false
|
||||
"scripts": ["service_worker.mjs"],
|
||||
|
||||
"service_worker": "service_worker.mjs",
|
||||
"type": "module"
|
||||
},
|
||||
"applications": {
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{8419486a-54e9-11e8-9401-ac9e17909436}"
|
||||
}
|
||||
|
@@ -115,6 +115,13 @@ class AppComponent extends Component {
|
||||
|
||||
this.clipScreenshot_click = async () => {
|
||||
try {
|
||||
// Firefox requires the <all_urls> host permission to take a
|
||||
// screenshot of the current page, however, this may change
|
||||
// in the future. Note that Firefox also forces this permission
|
||||
// to be optional.
|
||||
// See https://discourse.mozilla.org/t/browser-tabs-capturevisibletab-not-working-in-firefox-for-mv3/122965/3
|
||||
await bridge().browser().permissions.request({ origins: ['<all_urls>'] });
|
||||
|
||||
const baseUrl = await bridge().clipperServerBaseUrl();
|
||||
|
||||
await bridge().sendCommandToActiveTab({
|
||||
@@ -179,12 +186,14 @@ class AppComponent extends Component {
|
||||
}
|
||||
|
||||
async loadContentScripts() {
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/setUpEnvironment.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
|
||||
await bridge().tabsExecuteScript([
|
||||
'/content_scripts/setUpEnvironment.js',
|
||||
'/content_scripts/JSDOMParser.js',
|
||||
'/content_scripts/Readability.js',
|
||||
'/content_scripts/Readability-readerable.js',
|
||||
'/content_scripts/clipperUtils.js',
|
||||
'/content_scripts/index.js',
|
||||
]);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import getActiveTabs from '../../util/getActiveTabs.mjs';
|
||||
import joplinEnv from '../../util/joplinEnv.mjs';
|
||||
const { randomClipperPort } = require('./randomClipperPort');
|
||||
|
||||
function msleep(ms) {
|
||||
@@ -17,13 +19,12 @@ class Bridge {
|
||||
this.token_ = null;
|
||||
}
|
||||
|
||||
async init(browser, browserSupportsPromises, store) {
|
||||
async init(browser, store) {
|
||||
console.info('Popup: Init bridge');
|
||||
|
||||
this.browser_ = browser;
|
||||
this.dispatch_ = store.dispatch;
|
||||
this.store_ = store;
|
||||
this.browserSupportsPromises_ = browserSupportsPromises;
|
||||
this.clipperServerPort_ = null;
|
||||
this.clipperServerPortStatus_ = 'searching';
|
||||
|
||||
@@ -74,12 +75,7 @@ class Bridge {
|
||||
}
|
||||
};
|
||||
this.browser_.runtime.onMessage.addListener(this.browser_notify);
|
||||
const backgroundPage = await this.backgroundPage(this.browser_);
|
||||
|
||||
// Not sure why the getBackgroundPage() sometimes returns null, so
|
||||
// in that case default to "prod" environment, which means the live
|
||||
// extension won't be affected by this bug.
|
||||
this.env_ = backgroundPage ? backgroundPage.joplinEnv() : 'prod';
|
||||
this.env_ = joplinEnv();
|
||||
|
||||
console.info('Popup: Env:', this.env());
|
||||
|
||||
@@ -197,17 +193,6 @@ class Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
async backgroundPage(browser) {
|
||||
const bgp = browser.extension.getBackgroundPage();
|
||||
if (bgp) return bgp;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
browser.runtime.getBackgroundPage((bgp) => {
|
||||
resolve(bgp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.env_;
|
||||
}
|
||||
@@ -305,50 +290,26 @@ class Bridge {
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
async tabsExecuteScript(options) {
|
||||
if (this.browserSupportsPromises_) return this.browser().tabs.executeScript(options);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.browser().tabs.executeScript(options, () => {
|
||||
const e = this.browser().runtime.lastError;
|
||||
if (e) {
|
||||
const msg = [`tabsExecuteScript: Cannot load ${JSON.stringify(options)}`];
|
||||
if (e.message) msg.push(e.message);
|
||||
reject(new Error(msg.join(': ')));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
async tabsExecuteScript(files) {
|
||||
const activeTabs = await getActiveTabs(this.browser());
|
||||
await this.browser().scripting.executeScript({
|
||||
target: {
|
||||
tabId: activeTabs[0].id,
|
||||
},
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
async tabsQuery(options) {
|
||||
if (this.browserSupportsPromises_) return this.browser().tabs.query(options);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.browser().tabs.query(options, (tabs) => {
|
||||
resolve(tabs);
|
||||
});
|
||||
});
|
||||
return this.browser().tabs.query(options);
|
||||
}
|
||||
|
||||
async tabsSendMessage(tabId, command) {
|
||||
if (this.browserSupportsPromises_) return this.browser().tabs.sendMessage(tabId, command);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.browser().tabs.sendMessage(tabId, command, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
return this.browser().tabs.sendMessage(tabId, command);
|
||||
}
|
||||
|
||||
async tabsCreate(options) {
|
||||
if (this.browserSupportsPromises_) return this.browser().tabs.create(options);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.browser().tabs.create(options, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.browser().tabs.create(options);
|
||||
}
|
||||
|
||||
async folderTree() {
|
||||
@@ -356,29 +317,15 @@ class Bridge {
|
||||
}
|
||||
|
||||
async storageSet(keys) {
|
||||
if (this.browserSupportsPromises_) return this.browser().storage.local.set(keys);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.browser().storage.local.set(keys, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.browser().storage.local.set(keys);
|
||||
}
|
||||
|
||||
async storageGet(keys, defaultValue = null) {
|
||||
if (this.browserSupportsPromises_) {
|
||||
try {
|
||||
const r = await this.browser().storage.local.get(keys);
|
||||
return r;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
this.browser().storage.local.get(keys, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
try {
|
||||
const r = await this.browser().storage.local.get(keys);
|
||||
return r;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -112,7 +112,7 @@ async function main() {
|
||||
|
||||
console.info('Popup: Init bridge and restore state...');
|
||||
|
||||
await bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store);
|
||||
await bridge().init(window.browser ? window.browser : window.chrome, store);
|
||||
|
||||
console.info('Popup: Creating React app...');
|
||||
|
||||
|
@@ -1,25 +1,13 @@
|
||||
import joplinEnv from './util/joplinEnv.mjs';
|
||||
import getActiveTabs from './util/getActiveTabs.mjs';
|
||||
|
||||
let browser_ = null;
|
||||
if (typeof browser !== 'undefined') {
|
||||
browser_ = browser;
|
||||
browserSupportsPromises_ = true;
|
||||
} else if (typeof chrome !== 'undefined') {
|
||||
browser_ = chrome;
|
||||
browserSupportsPromises_ = false;
|
||||
}
|
||||
|
||||
let env_ = null;
|
||||
|
||||
// Make this function global so that it can be accessed
|
||||
// from the popup too.
|
||||
// https://stackoverflow.com/questions/6323184/communication-between-background-page-and-popup-page-in-a-chrome-extension
|
||||
window.joplinEnv = function() {
|
||||
if (env_) return env_;
|
||||
|
||||
const manifest = browser_.runtime.getManifest();
|
||||
env_ = manifest.name.indexOf('[DEV]') >= 0 ? 'dev' : 'prod';
|
||||
return env_;
|
||||
};
|
||||
|
||||
async function browserCaptureVisibleTabs(windowId) {
|
||||
const options = {
|
||||
format: 'jpeg',
|
||||
@@ -31,53 +19,19 @@ async function browserCaptureVisibleTabs(windowId) {
|
||||
// https://discourse.joplinapp.org/t/clip-screenshot-image-quality/12302/4
|
||||
quality: 92,
|
||||
};
|
||||
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, options);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
browser_.tabs.captureVisibleTab(windowId, options, (image) => {
|
||||
resolve(image);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function browserGetZoom(tabId) {
|
||||
if (browserSupportsPromises_) return browser_.tabs.getZoom(tabId);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
browser_.tabs.getZoom(tabId, (zoom) => {
|
||||
resolve(zoom);
|
||||
});
|
||||
});
|
||||
return browser_.tabs.captureVisibleTab(windowId, options);
|
||||
}
|
||||
|
||||
browser_.runtime.onInstalled.addListener(() => {
|
||||
if (window.joplinEnv() === 'dev') {
|
||||
browser_.browserAction.setIcon({
|
||||
if (joplinEnv() === 'dev') {
|
||||
browser_.action.setIcon({
|
||||
path: 'icons/32-dev.png',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function getImageSize(dataUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
|
||||
image.onload = function() {
|
||||
resolve({ width: image.width, height: image.height });
|
||||
};
|
||||
|
||||
image.onerror = function(event) {
|
||||
reject(event);
|
||||
};
|
||||
|
||||
image.src = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
browser_.runtime.onMessage.addListener(async (command) => {
|
||||
if (command.name === 'screenshotArea') {
|
||||
const browserZoom = await browserGetZoom();
|
||||
|
||||
// The dimensions of the image returned by Firefox are the regular ones,
|
||||
// while the one returned by Chrome depend on the screen pixel ratio. So
|
||||
// it would return a 600*400 image if the window dimensions are 300x200
|
||||
@@ -90,15 +44,18 @@ browser_.runtime.onMessage.addListener(async (command) => {
|
||||
//
|
||||
// The crop rectangle is always in real pixels, so we need to multiply
|
||||
// it by the ratio we've calculated.
|
||||
//
|
||||
// 8/3/2024: With manifest v3, we don't have access to DOM APIs in Chrome.
|
||||
// As a result, we can't easily calculate the size of the captured image.
|
||||
// We instead base the crop region exclusively on window.devicePixelRatio,
|
||||
// which seems to work in modern Firefox and Chrome.
|
||||
const imageDataUrl = await browserCaptureVisibleTabs(null);
|
||||
const imageSize = await getImageSize(imageDataUrl);
|
||||
const imagePixelRatio = imageSize.width / command.content.windowInnerWidth;
|
||||
|
||||
const content = { ...command.content };
|
||||
content.image_data_url = imageDataUrl;
|
||||
if ('url' in content) content.source_url = content.url;
|
||||
|
||||
const ratio = browserZoom * imagePixelRatio;
|
||||
const ratio = content.devicePixelRatio;
|
||||
const newArea = { ...command.content.crop_rect };
|
||||
newArea.x *= ratio;
|
||||
newArea.y *= ratio;
|
||||
@@ -117,19 +74,8 @@ browser_.runtime.onMessage.addListener(async (command) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function getActiveTabs() {
|
||||
const options = { active: true, currentWindow: true };
|
||||
if (browserSupportsPromises_) return browser_.tabs.query(options);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
browser_.tabs.query(options, (tabs) => {
|
||||
resolve(tabs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function sendClipMessage(clipType) {
|
||||
const tabs = await getActiveTabs();
|
||||
const tabs = await getActiveTabs(browser_);
|
||||
if (!tabs || !tabs.length) {
|
||||
console.error('No active tabs');
|
||||
return;
|
7
packages/app-clipper/util/getActiveTabs.mjs
Normal file
7
packages/app-clipper/util/getActiveTabs.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
const getActiveTabs = async (browser) => {
|
||||
const options = { active: true, currentWindow: true };
|
||||
return await browser.tabs.query(options);
|
||||
}
|
||||
|
||||
export default getActiveTabs;
|
3
packages/app-clipper/util/joplinEnv.mjs
Normal file
3
packages/app-clipper/util/joplinEnv.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
// AUTOGENERATED by release-clipper
|
||||
|
||||
export default () => 'dev';
|
@@ -201,8 +201,10 @@ class Application extends BaseApplication {
|
||||
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
//
|
||||
// Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables.
|
||||
|
||||
const css = `.CodeMirror *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const css = `.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
@@ -277,17 +279,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
try {
|
||||
const devPluginOptions = { devMode: true, builtIn: false };
|
||||
|
||||
if (Setting.value('plugins.devPluginPaths')) {
|
||||
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
|
||||
await service.loadAndRunPlugins(paths, pluginSettings, devPluginOptions);
|
||||
}
|
||||
|
||||
// Also load dev plugins that have passed via command line arguments
|
||||
if (Setting.value('startupDevPlugins')) {
|
||||
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, devPluginOptions);
|
||||
}
|
||||
await service.loadAndRunDevPlugins(pluginSettings);
|
||||
} catch (error) {
|
||||
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell } from 'electron';
|
||||
import { dirname, isUncPath, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, dialog, shell, MessageBoxSyncOptions } from 'electron';
|
||||
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { urlDecode } from '@joplin/lib/string-utils';
|
||||
import * as Sentry from '@sentry/electron/main';
|
||||
@@ -23,6 +23,10 @@ interface OpenDialogOptions {
|
||||
filters?: any[];
|
||||
}
|
||||
|
||||
interface MessageDialogOptions extends Omit<MessageBoxSyncOptions, 'message'> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class Bridge {
|
||||
|
||||
private electronWrapper_: ElectronAppWrapper;
|
||||
@@ -84,11 +88,6 @@ export class Bridge {
|
||||
return this.rootProfileDir_;
|
||||
}
|
||||
|
||||
private logWarning(...message: string[]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('bridge:', ...message);
|
||||
}
|
||||
|
||||
public electronApp() {
|
||||
return this.electronWrapper_;
|
||||
}
|
||||
@@ -228,7 +227,6 @@ export class Bridge {
|
||||
}
|
||||
|
||||
public async showSaveDialog(options: any) {
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
|
||||
const { filePath } = await dialog.showSaveDialog(this.window(), options);
|
||||
@@ -239,7 +237,6 @@ export class Bridge {
|
||||
}
|
||||
|
||||
public async showOpenDialog(options: OpenDialogOptions = null) {
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
let fileType = 'file';
|
||||
if (options.properties && options.properties.includes('openDirectory')) fileType = 'directory';
|
||||
@@ -253,13 +250,12 @@ export class Bridge {
|
||||
}
|
||||
|
||||
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
|
||||
private showMessageBox_(window: any, options: any): number {
|
||||
const { dialog } = require('electron');
|
||||
private showMessageBox_(window: any, options: MessageDialogOptions): number {
|
||||
if (!window) window = this.window();
|
||||
return dialog.showMessageBoxSync(window, options);
|
||||
return dialog.showMessageBoxSync(window, { message: '', ...options });
|
||||
}
|
||||
|
||||
public showErrorMessageBox(message: string, options: any = null) {
|
||||
public showErrorMessageBox(message: string, options: MessageDialogOptions = null) {
|
||||
options = {
|
||||
buttons: [_('OK')],
|
||||
...options,
|
||||
@@ -272,7 +268,7 @@ export class Bridge {
|
||||
});
|
||||
}
|
||||
|
||||
public showConfirmMessageBox(message: string, options: any = null) {
|
||||
public showConfirmMessageBox(message: string, options: MessageDialogOptions = null) {
|
||||
options = {
|
||||
buttons: [_('OK'), _('Cancel')],
|
||||
...options,
|
||||
@@ -287,9 +283,7 @@ export class Bridge {
|
||||
}
|
||||
|
||||
/* returns the index of the clicked button */
|
||||
public showMessageBox(message: string, options: any = null) {
|
||||
if (options === null) options = {};
|
||||
|
||||
public showMessageBox(message: string, options: MessageDialogOptions = {}) {
|
||||
const result = this.showMessageBox_(this.window(), { type: 'question',
|
||||
message: message,
|
||||
buttons: [_('OK'), _('Cancel')], ...options });
|
||||
@@ -331,13 +325,10 @@ export class Bridge {
|
||||
fullPath = fileUriToPath(urlDecode(fullPath), shim.platformName());
|
||||
}
|
||||
fullPath = normalize(fullPath);
|
||||
// On Windows, \\example.com\... links can map to network drives. Opening files on these
|
||||
// drives can lead to arbitrary remote code execution.
|
||||
const isUntrustedUncPath = isUncPath(fullPath);
|
||||
if (isUntrustedUncPath) {
|
||||
this.logWarning(`Not opening external file link: ${fullPath} -- it starts with two \\s, so could be to a network drive.`);
|
||||
return 'Refusing to open file on a network drive.';
|
||||
} else if (await pathExists(fullPath)) {
|
||||
|
||||
// Note: pathExists is intended to mitigate a security issue related to network drives
|
||||
// on Windows.
|
||||
if (await pathExists(fullPath)) {
|
||||
return shell.openPath(fullPath);
|
||||
} else {
|
||||
return 'Path does not exist.';
|
||||
|
24
packages/app-desktop/commands/emptyTrash.ts
Normal file
24
packages/app-desktop/commands/emptyTrash.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import emptyTrash from '@joplin/lib/services/trash/emptyTrash';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'emptyTrash',
|
||||
label: () => _('Empty trash'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async () => {
|
||||
const ok = await bridge().showConfirmMessageBox(_('This will permanently delete all items in the trash. Continue?'), {
|
||||
buttons: [
|
||||
_('Empty trash'),
|
||||
_('Cancel'),
|
||||
],
|
||||
});
|
||||
|
||||
if (ok) await emptyTrash();
|
||||
},
|
||||
};
|
||||
};
|
@@ -1,6 +1,7 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as copyDevCommand from './copyDevCommand';
|
||||
import * as editProfileConfig from './editProfileConfig';
|
||||
import * as emptyTrash from './emptyTrash';
|
||||
import * as exportFolders from './exportFolders';
|
||||
import * as exportNotes from './exportNotes';
|
||||
import * as focusElement from './focusElement';
|
||||
@@ -19,6 +20,7 @@ import * as toggleSafeMode from './toggleSafeMode';
|
||||
const index: any[] = [
|
||||
copyDevCommand,
|
||||
editProfileConfig,
|
||||
emptyTrash,
|
||||
exportFolders,
|
||||
exportNotes,
|
||||
focusElement,
|
||||
|
@@ -58,6 +58,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
|
||||
private async checkSyncConfig_() {
|
||||
if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) {
|
||||
const isAuthenticated = await reg.syncTarget().isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
return this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'JoplinCloudLogin',
|
||||
});
|
||||
}
|
||||
}
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
@@ -336,10 +345,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.setState({ needRestart: true });
|
||||
}
|
||||
shared.updateSettingValue(this, key, value);
|
||||
|
||||
if (md.autoSave) {
|
||||
shared.scheduleSaveSettings(this);
|
||||
}
|
||||
};
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const styled = require('styled-components').default;
|
||||
@@ -72,23 +71,6 @@ export const StyledListItemIcon = styled.i`
|
||||
export default function Sidebar(props: Props) {
|
||||
const buttons: any[] = [];
|
||||
|
||||
const sortedSections = useMemo(() => {
|
||||
const output = props.sections.slice();
|
||||
output.sort((a: any, b: any) => {
|
||||
const s1 = a.source || SettingSectionSource.Default;
|
||||
const s2 = b.source || SettingSectionSource.Default;
|
||||
if (s1 === SettingSectionSource.Default && s2 === SettingSectionSource.Default) return props.sections.indexOf(s1) - props.sections.indexOf(s2);
|
||||
if (s1 === SettingSectionSource.Default && s2 === SettingSectionSource.Plugin) return -1;
|
||||
if (s1 === SettingSectionSource.Plugin && s2 === SettingSectionSource.Default) return +1;
|
||||
|
||||
const l1 = Setting.sectionNameToLabel(a.name);
|
||||
const l2 = Setting.sectionNameToLabel(b.name);
|
||||
if (s1 === SettingSectionSource.Plugin && s2 === SettingSectionSource.Plugin) return l1.toLowerCase() < l2.toLowerCase() ? -1 : +1;
|
||||
return 0;
|
||||
});
|
||||
return output;
|
||||
}, [props.sections]);
|
||||
|
||||
function renderButton(section: any) {
|
||||
const selected = props.selection === section.name;
|
||||
return (
|
||||
@@ -121,7 +103,7 @@ export default function Sidebar(props: Props) {
|
||||
|
||||
let pluginDividerAdded = false;
|
||||
|
||||
for (const section of sortedSections) {
|
||||
for (const section of props.sections) {
|
||||
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
|
||||
buttons.push(renderDivider('divider-plugins'));
|
||||
pluginDividerAdded = true;
|
||||
|
@@ -6,6 +6,7 @@ import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
||||
import Button, { ButtonLevel } from '../../../Button/Button';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled = 1,
|
||||
@@ -20,10 +21,6 @@ export enum UpdateState {
|
||||
HasBeenUpdated = 4,
|
||||
}
|
||||
|
||||
export interface ItemEvent {
|
||||
item: PluginItem;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item?: PluginItem;
|
||||
manifest?: PluginManifest;
|
||||
@@ -48,15 +45,6 @@ function manifestToItem(manifest: PluginManifest): PluginItem {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
deleted: boolean;
|
||||
devMode: boolean;
|
||||
builtIn: boolean;
|
||||
hasBeenUpdated: boolean;
|
||||
}
|
||||
|
||||
const CellRoot = styled.div<{ isCompatible: boolean }>`
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
@@ -4,15 +4,16 @@ import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSett
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
import SearchPlugins from './SearchPlugins';
|
||||
import PluginBox, { ItemEvent, UpdateState } from './PluginBox';
|
||||
import PluginBox, { UpdateState } from './PluginBox';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../../../Button/Button';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import produce from 'immer';
|
||||
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||
import { PluginItem } from './PluginBox';
|
||||
import { PluginItem, ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useOnInstallHandler, { OnPluginSettingChangeEvent } from './useOnInstallHandler';
|
||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/useOnDeleteHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import StyledMessage from '../../../style/StyledMessage';
|
||||
import StyledLink from '../../../style/StyledLink';
|
||||
@@ -59,7 +60,7 @@ let repoApi_: RepositoryApi = null;
|
||||
|
||||
function repoApi(): RepositoryApi {
|
||||
if (repoApi_) return repoApi_;
|
||||
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
|
||||
repoApi_ = RepositoryApi.ofDefaultJoplinRepo(Setting.value('tempDir'));
|
||||
// repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir'));
|
||||
return repoApi_;
|
||||
}
|
||||
@@ -170,20 +171,6 @@ export default function(props: Props) {
|
||||
};
|
||||
}, [manifestsLoaded, pluginItems, pluginService.appVersion]);
|
||||
|
||||
const onDelete = useCallback(async (event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.manifest.name));
|
||||
if (!confirm) return;
|
||||
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
if (!draft[item.manifest.id]) draft[item.manifest.id] = defaultPluginSetting();
|
||||
draft[item.manifest.id].deleted = true;
|
||||
});
|
||||
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onToggle = useCallback((event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
|
||||
@@ -215,14 +202,14 @@ export default function(props: Props) {
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onBrowsePlugins = useCallback(() => {
|
||||
void bridge().openExternal('https://github.com/joplin/plugins/blob/master/README.md#plugins');
|
||||
void bridge().openExternal('https://joplinapp.org/plugins/');
|
||||
}, []);
|
||||
|
||||
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}, [pluginService, props.onChange]);
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettings, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
|
||||
const onToolsClick = useCallback(async () => {
|
||||
|
@@ -8,7 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import PluginBox, { InstallState } from './PluginBox';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Root = styled.div`
|
||||
@@ -32,14 +32,6 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
function sortManifestResults(results: PluginManifest[]): PluginManifest[] {
|
||||
return results.sort((m1, m2) => {
|
||||
if (m1._recommended && !m2._recommended) return -1;
|
||||
if (!m1._recommended && m2._recommended) return +1;
|
||||
return m1.name.toLowerCase() < m2.name.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [searchStarted, setSearchStarted] = useState(false);
|
||||
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
||||
@@ -57,7 +49,7 @@ export default function(props: Props) {
|
||||
setSearchResultCount(null);
|
||||
} else {
|
||||
const r = await props.repoApi().search(props.searchQuery);
|
||||
setManifests(sortManifestResults(r));
|
||||
setManifests(r);
|
||||
setSearchResultCount(r.length);
|
||||
}
|
||||
});
|
||||
|
@@ -1,63 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import PluginService, { defaultPluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import produce from 'immer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ItemEvent } from './PluginBox';
|
||||
|
||||
const logger = Logger.create('useOnInstallHandler');
|
||||
|
||||
export interface OnPluginSettingChangeEvent {
|
||||
value: PluginSettings;
|
||||
}
|
||||
|
||||
type OnPluginSettingChangeHandler = (event: OnPluginSettingChangeEvent)=> void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: OnPluginSettingChangeHandler, isUpdate: boolean) {
|
||||
return useCallback(async (event: ItemEvent) => {
|
||||
const pluginId = event.item.manifest.id;
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: true,
|
||||
};
|
||||
});
|
||||
|
||||
let installError = null;
|
||||
|
||||
try {
|
||||
if (isUpdate) {
|
||||
await PluginService.instance().updatePluginFromRepo(repoApi(), pluginId);
|
||||
} else {
|
||||
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
|
||||
}
|
||||
} catch (error) {
|
||||
installError = error;
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
if (!installError) {
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
draft[pluginId] = defaultPluginSetting();
|
||||
if (isUpdate) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
draft[pluginId].enabled = pluginSettings[pluginId].enabled;
|
||||
}
|
||||
draft[pluginId].hasBeenUpdated = true;
|
||||
}
|
||||
});
|
||||
|
||||
onPluginSettingsChange({ value: newSettings });
|
||||
}
|
||||
|
||||
setInstallingPluginIds((prev: any) => {
|
||||
return {
|
||||
...prev, [pluginId]: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (installError) alert(_('Could not install plugin: %s', installError.message));
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, onPluginSettingsChange]);
|
||||
}
|
32
packages/app-desktop/gui/JoplinCloudLoginScreen.scss
Normal file
32
packages/app-desktop/gui/JoplinCloudLoginScreen.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.login-page {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--joplin-background-color);
|
||||
color: var(--joplin-color);
|
||||
|
||||
> .page-container {
|
||||
height: calc(100% - (var(--joplin-margin) * 2));
|
||||
flex: 1;
|
||||
padding: var(--joplin-config-screen-padding);
|
||||
|
||||
> .text {
|
||||
font-size: var(--joplin-font-size);
|
||||
}
|
||||
|
||||
> .buttons-container {
|
||||
margin-bottom: calc(var(--joplin-font-size) * 2);
|
||||
display: flex;
|
||||
|
||||
> button:first-child {
|
||||
margin-right: calc(var(--joplin-font-size) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
> .bold {
|
||||
font-weight: bold;
|
||||
font-size: calc(var(--joplin-font-size) * 1.3);
|
||||
}
|
||||
}
|
||||
}
|
115
packages/app-desktop/gui/JoplinCloudLoginScreen.tsx
Normal file
115
packages/app-desktop/gui/JoplinCloudLoginScreen.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { clipboard } from 'electron';
|
||||
import Button, { ButtonLevel } from './Button/Button';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import { Dispatch } from 'redux';
|
||||
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import { AppState } from '../app.reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
joplinCloudWebsite: string;
|
||||
joplinCloudApi: string;
|
||||
}
|
||||
|
||||
const JoplinCloudScreenComponent = (props: Props) => {
|
||||
|
||||
const confirmUrl = (applicationAuthId: string) => `${props.joplinCloudWebsite}/applications/${applicationAuthId}/confirm`;
|
||||
const applicationAuthUrl = (applicationAuthId: string) => `${props.joplinCloudApi}/api/application_auth/${applicationAuthId}`;
|
||||
|
||||
const [intervalIdentifier, setIntervalIdentifier] = useState(undefined);
|
||||
const [state, dispatch] = useReducer(reducer, defaultState);
|
||||
|
||||
const applicationAuthId = useMemo(() => uuidgen(), []);
|
||||
|
||||
const periodicallyCheckForCredentials = () => {
|
||||
if (intervalIdentifier) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await checkIfLoginWasSuccessful(applicationAuthUrl(applicationAuthId));
|
||||
if (response && response.success) {
|
||||
dispatch({ type: 'COMPLETED' });
|
||||
clearInterval(interval);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
dispatch({ type: 'ERROR', payload: error.message });
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2 * 1000);
|
||||
|
||||
setIntervalIdentifier(interval);
|
||||
};
|
||||
|
||||
const onButtonUsed = () => {
|
||||
if (state.next === 'LINK_USED') {
|
||||
dispatch({ type: 'LINK_USED' });
|
||||
}
|
||||
periodicallyCheckForCredentials();
|
||||
};
|
||||
|
||||
const onAuthorizeClicked = async () => {
|
||||
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
|
||||
bridge().openExternal(url);
|
||||
onButtonUsed();
|
||||
};
|
||||
|
||||
const onCopyToClipboardClicked = async () => {
|
||||
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
|
||||
clipboard.writeText(url);
|
||||
onButtonUsed();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(intervalIdentifier);
|
||||
};
|
||||
}, [intervalIdentifier]);
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="page-container">
|
||||
<p className="text">{_('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:')}</p>
|
||||
<div className="buttons-container">
|
||||
<Button
|
||||
onClick={onAuthorizeClicked}
|
||||
title={_('Authorise')}
|
||||
iconName='fa fa-external-link-alt'
|
||||
level={ButtonLevel.Primary}
|
||||
/>
|
||||
<Button
|
||||
onClick={onCopyToClipboardClicked}
|
||||
title={_('Copy link to website')}
|
||||
iconName='fa fa-clone'
|
||||
level={ButtonLevel.Secondary}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<p className={state.className}>{state.message()}
|
||||
{state.active === 'ERROR' ? (
|
||||
<span className={state.className}>{state.errorMessage}</span>
|
||||
) : null}
|
||||
</p>
|
||||
{state.active === 'LINK_USED' ? <div id="loading-animation" /> : null}
|
||||
</div>
|
||||
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
joplinCloudWebsite: state.settings['sync.10.website'],
|
||||
joplinCloudApi: state.settings['sync.10.path'],
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(JoplinCloudScreenComponent);
|
@@ -13,7 +13,7 @@ import Sidebar from '../Sidebar/Sidebar';
|
||||
import UserWebview from '../../services/plugins/UserWebview';
|
||||
import UserWebviewDialog from '../../services/plugins/UserWebviewDialog';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { StateLastDeletion, stateUtils } from '@joplin/lib/reducer';
|
||||
import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
|
||||
@@ -45,6 +45,10 @@ import restart from '../../services/restart';
|
||||
const { connect } = require('react-redux');
|
||||
import PromptDialog from '../PromptDialog';
|
||||
import NotePropertiesDialog from '../NotePropertiesDialog';
|
||||
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import validateColumns from '../NoteListHeader/utils/validateColumns';
|
||||
import TrashNotification from '../TrashNotification/TrashNotification';
|
||||
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
@@ -83,7 +87,13 @@ interface Props {
|
||||
processingShareInvitationResponse: boolean;
|
||||
isResettingLayout: boolean;
|
||||
listRendererId: string;
|
||||
lastDeletion: StateLastDeletion;
|
||||
lastDeletionNotificationTime: number;
|
||||
selectedFolderId: string;
|
||||
mustUpgradeAppMessage: string;
|
||||
notesSortOrderField: string;
|
||||
notesSortOrderReverse: boolean;
|
||||
notesColumns: NoteListColumns;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -732,6 +742,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
themeId={this.props.themeId}
|
||||
listRendererId={this.props.listRendererId}
|
||||
startupPluginsLoaded={this.props.startupPluginsLoaded}
|
||||
notesSortOrderField={this.props.notesSortOrderField}
|
||||
notesSortOrderReverse={this.props.notesSortOrderReverse}
|
||||
columns={this.props.notesColumns}
|
||||
selectedFolderId={this.props.selectedFolderId}
|
||||
/>;
|
||||
},
|
||||
|
||||
@@ -880,6 +894,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
|
||||
|
||||
<TrashNotification
|
||||
lastDeletion={this.props.lastDeletion}
|
||||
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
|
||||
themeId={this.props.themeId}
|
||||
dispatch={this.props.dispatch as any}
|
||||
/>
|
||||
{messageComp}
|
||||
{layoutComp}
|
||||
{pluginDialog}
|
||||
@@ -918,7 +938,13 @@ const mapStateToProps = (state: AppState) => {
|
||||
needApiAuth: state.needApiAuth,
|
||||
isResettingLayout: state.isResettingLayout,
|
||||
listRendererId: state.settings['notes.listRendererId'],
|
||||
lastDeletion: state.lastDeletion,
|
||||
lastDeletionNotificationTime: state.lastDeletionNotificationTime,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
|
||||
notesSortOrderField: state.settings['notes.sortOrder.field'],
|
||||
notesSortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -17,7 +17,7 @@ export const runtime = (): CommandRuntime => {
|
||||
const folder = await Folder.load(folderId);
|
||||
if (!folder) throw new Error(`No such folder: ${folderId}`);
|
||||
|
||||
let deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
|
||||
let deleteMessage = _('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', substrWithEllipsis(folder.title, 0, 32));
|
||||
if (folderId === context.state.settings['sync.10.inboxId']) {
|
||||
deleteMessage = _('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.');
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => {
|
||||
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
||||
if (!ok) return;
|
||||
|
||||
await Folder.delete(folderId);
|
||||
await Folder.delete(folderId, { toTrash: true, sourceDescription: 'deleteFolder command' });
|
||||
},
|
||||
enabledCondition: '!folderIsReadOnly',
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'deleteNote',
|
||||
@@ -13,20 +12,17 @@ export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteIds: string[] = null) => {
|
||||
if (noteIds === null) noteIds = context.state.selectedNoteIds;
|
||||
|
||||
if (!noteIds.length) return;
|
||||
await Note.batchDelete(noteIds, { toTrash: true, sourceDescription: 'deleteNote command' });
|
||||
|
||||
const msg = await Note.deleteMessage(noteIds);
|
||||
if (!msg) return;
|
||||
|
||||
const ok = bridge().showConfirmMessageBox(msg, {
|
||||
buttons: [_('Delete'), _('Cancel')],
|
||||
defaultId: 1,
|
||||
context.dispatch({
|
||||
type: 'ITEMS_TRASHED',
|
||||
value: {
|
||||
noteIds,
|
||||
folderIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
if (!ok) return;
|
||||
await Note.batchDelete(noteIds);
|
||||
},
|
||||
enabledCondition: '!noteIsReadOnly',
|
||||
enabledCondition: '!noteIsReadOnly && !inTrash && someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import time from '@joplin/lib/time';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'editAlarm',
|
||||
@@ -29,7 +30,7 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
buttons: ['ok', 'cancel', 'clear'],
|
||||
value: note.todo_due ? new Date(note.todo_due) : defaultDate,
|
||||
onClose: async (answer: any, buttonType: string) => {
|
||||
let newNote = null;
|
||||
let newNote: NoteEntity = null;
|
||||
|
||||
if (buttonType === 'clear') {
|
||||
newNote = {
|
||||
|
@@ -20,10 +20,13 @@ import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as openPdfViewer from './openPdfViewer';
|
||||
import * as openTag from './openTag';
|
||||
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
import * as renameTag from './renameTag';
|
||||
import * as resetLayout from './resetLayout';
|
||||
import * as restoreFolder from './restoreFolder';
|
||||
import * as restoreNote from './restoreNote';
|
||||
import * as revealResourceFile from './revealResourceFile';
|
||||
import * as search from './search';
|
||||
import * as setTags from './setTags';
|
||||
@@ -66,10 +69,13 @@ const index: any[] = [
|
||||
openNote,
|
||||
openPdfViewer,
|
||||
openTag,
|
||||
permanentlyDeleteNote,
|
||||
print,
|
||||
renameFolder,
|
||||
renameTag,
|
||||
resetLayout,
|
||||
restoreFolder,
|
||||
restoreNote,
|
||||
revealResourceFile,
|
||||
search,
|
||||
setTags,
|
||||
|
@@ -3,6 +3,8 @@ import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
export const newNoteEnabledConditions = 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly && !folderIsTrash';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newNote',
|
||||
label: () => _('New note'),
|
||||
@@ -36,6 +38,6 @@ export const runtime = (): CommandRuntime => {
|
||||
type: 'NOTE_SORT',
|
||||
});
|
||||
},
|
||||
enabledCondition: 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly',
|
||||
enabledCondition: newNoteEnabledConditions,
|
||||
};
|
||||
};
|
||||
|
@@ -13,6 +13,6 @@ export const runtime = (): CommandRuntime => {
|
||||
parentId = parentId || context.state.selectedFolderId;
|
||||
return CommandService.instance().execute('newFolder', parentId);
|
||||
},
|
||||
enabledCondition: '!folderIsReadOnly',
|
||||
enabledCondition: '!folderIsReadOnly && !folderIsTrash',
|
||||
};
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { newNoteEnabledConditions } from './newNote';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newTodo',
|
||||
@@ -12,6 +13,6 @@ export const runtime = (): CommandRuntime => {
|
||||
execute: async (_context: CommandContext, body = '') => {
|
||||
return CommandService.instance().execute('newNote', body, true);
|
||||
},
|
||||
enabledCondition: 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly',
|
||||
enabledCondition: newNoteEnabledConditions,
|
||||
};
|
||||
};
|
||||
|
@@ -0,0 +1,30 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'permanentlyDeleteNote',
|
||||
label: () => _('Permanently delete note'),
|
||||
iconName: 'fa-times',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteIds: string[] = null) => {
|
||||
if (noteIds === null) noteIds = context.state.selectedNoteIds;
|
||||
if (!noteIds.length) return;
|
||||
const msg = await Note.permanentlyDeleteMessage(noteIds);
|
||||
|
||||
const ok = bridge().showConfirmMessageBox(msg, {
|
||||
buttons: [_('Delete'), _('Cancel')],
|
||||
defaultId: 1,
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
await Note.batchDelete(noteIds, { toTrash: false, sourceDescription: 'permanentlyDeleteNote command' });
|
||||
}
|
||||
},
|
||||
enabledCondition: '(!noteIsReadOnly || inTrash) && someNotesSelected',
|
||||
};
|
||||
};
|
@@ -0,0 +1,24 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'restoreFolder',
|
||||
label: () => _('Restore notebook'),
|
||||
iconName: 'fas fa-trash-restore',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, folderId: string = null) => {
|
||||
if (folderId === null) folderId = context.state.selectedFolderId;
|
||||
|
||||
const folder = await Folder.load(folderId);
|
||||
if (!folder) throw new Error(`No such folder: ${folderId}`);
|
||||
await restoreItems(ModelType.Folder, [folder]);
|
||||
},
|
||||
enabledCondition: 'folderIsDeleted',
|
||||
};
|
||||
};
|
23
packages/app-desktop/gui/MainScreen/commands/restoreNote.ts
Normal file
23
packages/app-desktop/gui/MainScreen/commands/restoreNote.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'restoreNote',
|
||||
label: () => _('Restore note'),
|
||||
iconName: 'fas fa-trash-restore',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteIds: string[] = null) => {
|
||||
if (noteIds === null) noteIds = context.state.selectedNoteIds;
|
||||
const notes: NoteEntity[] = await Note.byIds(noteIds, { fields: ['id', 'parent_id'] });
|
||||
await restoreItems(ModelType.Note, notes);
|
||||
},
|
||||
enabledCondition: 'allSelectedNotesAreDeleted',
|
||||
};
|
||||
};
|
@@ -66,6 +66,6 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'someNotesSelected',
|
||||
enabledCondition: 'someNotesSelected && !inTrash',
|
||||
};
|
||||
};
|
||||
|
@@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'joplinServerConnected && someNotesSelected',
|
||||
enabledCondition: 'joplinServerConnected && someNotesSelected && !noteIsDeleted',
|
||||
};
|
||||
};
|
||||
|
@@ -837,6 +837,8 @@ function useMenu(props: Props) {
|
||||
separator(),
|
||||
menuItemDic.showNoteProperties,
|
||||
menuItemDic.showNoteContentProperties,
|
||||
separator(),
|
||||
menuItemDic.permanentlyDeleteNote,
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
|
@@ -0,0 +1,21 @@
|
||||
import normalizeAccelerator from './normalizeAccelerator';
|
||||
import { CodeMirrorVersion } from './types';
|
||||
|
||||
describe('normalizeAccelerator', () => {
|
||||
test.each([
|
||||
['Z', { v6: 'z', v5: 'Z' }],
|
||||
['Alt+A', { v6: 'Alt-a', v5: 'Alt-A' }],
|
||||
['Shift+A', { v6: 'Shift-a', v5: 'Shift-A' }],
|
||||
['Shift+Up', { v6: 'Shift-Up', v5: 'Shift-Up' }],
|
||||
])(
|
||||
'should convert single-letter key names to lowercase for CM6, keep case unchanged for CM5 (%j)',
|
||||
(original, expected) => {
|
||||
expect(normalizeAccelerator(
|
||||
original, CodeMirrorVersion.CodeMirror6,
|
||||
)).toBe(expected.v6);
|
||||
expect(normalizeAccelerator(
|
||||
original, CodeMirrorVersion.CodeMirror5,
|
||||
)).toBe(expected.v5);
|
||||
},
|
||||
);
|
||||
});
|
@@ -0,0 +1,35 @@
|
||||
import { CodeMirrorVersion } from './types';
|
||||
|
||||
// CodeMirror and Electron register accelerators slightly different
|
||||
// CodeMirror requires a - between keys while Electron want's a +
|
||||
// CodeMirror doesn't recognize Option (it uses Alt instead)
|
||||
// CodeMirror requires Shift to be first
|
||||
// CodeMirror 6 requires Shift if the key name is uppercase.
|
||||
const normalizeAccelerator = (accelerator: string, editorVersion: CodeMirrorVersion) => {
|
||||
const command = accelerator.replace(/\+/g, '-').replace('Option', 'Alt');
|
||||
// From here is taken out of codemirror/lib/codemirror.js, modified
|
||||
// to also support CodeMirror 6.
|
||||
const parts = command.split(/-(?!$)/);
|
||||
let name = parts[parts.length - 1];
|
||||
|
||||
// In CodeMirror 6, an uppercase single-letter key name makes the editor
|
||||
// require the shift key to activate the shortcut. If a key name like `Up`,
|
||||
// however, `.toLowerCase` breaks the shortcut.
|
||||
if (editorVersion === CodeMirrorVersion.CodeMirror6 && name.length === 1) {
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
let alt, ctrl, shift, cmd;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const mod = parts[i];
|
||||
if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } else if (/^a(lt)?$/i.test(mod)) { alt = true; } else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } else if (/^s(hift)?$/i.test(mod)) { shift = true; } else { throw new Error(`Unrecognized modifier name: ${mod}`); }
|
||||
}
|
||||
if (alt) { name = `Alt-${name}`; }
|
||||
if (ctrl) { name = `Ctrl-${name}`; }
|
||||
if (cmd) { name = `Cmd-${name}`; }
|
||||
if (shift) { name = `Shift-${name}`; }
|
||||
return name;
|
||||
// End of code taken from codemirror/lib/codemirror.js
|
||||
};
|
||||
|
||||
export default normalizeAccelerator;
|
@@ -9,3 +9,8 @@ export function defaultRenderedBody(): RenderedBody {
|
||||
pluginAssets: [],
|
||||
};
|
||||
}
|
||||
|
||||
export enum CodeMirrorVersion {
|
||||
CodeMirror5,
|
||||
CodeMirror6,
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
|
||||
import { ContextMenuEvent, ContextMenuParams } from 'electron';
|
||||
import { ContextMenuParams, Event } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
@@ -69,7 +69,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
}
|
||||
|
||||
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
|
||||
async function onContextMenu(event: Event, params: ContextMenuParams) {
|
||||
if (!pointerInsideEditor(params)) return;
|
||||
|
||||
// Don't show the default menu.
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
|
||||
|
||||
const logger = Logger.create('useEditorSearch');
|
||||
|
||||
export default function useEditorSearch(CodeMirror: any) {
|
||||
// Registers a helper CodeMirror extension to be used with
|
||||
// useEditorSearchHandler.
|
||||
|
||||
export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulation) {
|
||||
|
||||
const [markers, setMarkers] = useState([]);
|
||||
const [overlay, setOverlay] = useState(null);
|
||||
@@ -73,7 +77,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
// If we run out of matches then just highlight the final match
|
||||
break;
|
||||
}
|
||||
match = cursor.pos;
|
||||
match = { from: cursor.from(), to: cursor.to() };
|
||||
}
|
||||
|
||||
if (match) {
|
||||
@@ -81,7 +85,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
if (withSelection) {
|
||||
cm.setSelection(match.from, match.to);
|
||||
} else {
|
||||
cm.scrollTo(match);
|
||||
cm.scrollIntoView(match);
|
||||
}
|
||||
}
|
||||
return cm.markText(match.from, match.to, { className: 'cm-search-marker-selected' });
|
||||
@@ -107,7 +111,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
CodeMirror.defineExtension('setMarkers', function(keywords: any, options: any) {
|
||||
CodeMirror?.defineExtension('setMarkers', function(keywords: any, options: any) {
|
||||
if (!options) {
|
||||
options = { selectedIndex: 0, searchTimestamp: 0 };
|
||||
}
|
||||
@@ -172,7 +176,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
// These operations are pretty slow, so we won't add use them until the user
|
||||
// has finished typing, 500ms is probably enough time
|
||||
const timeout = shim.setTimeout(() => {
|
||||
const scrollMarks = this.showMatchesOnScrollbar(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||
const scrollMarks = this.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||
const overlay = searchOverlay(searchTerm);
|
||||
this.addOverlay(overlay);
|
||||
setOverlay(overlay);
|
@@ -0,0 +1,66 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import { RenderedBody } from './types';
|
||||
const debounce = require('debounce');
|
||||
|
||||
interface Props {
|
||||
setLocalSearchResultCount(count: number): void;
|
||||
searchMarkers: any;
|
||||
webviewRef: RefObject<any>;
|
||||
editorRef: RefObject<any>;
|
||||
|
||||
noteContent: string;
|
||||
renderedBody: RenderedBody;
|
||||
}
|
||||
|
||||
const useEditorSearchHandler = (props: Props) => {
|
||||
const { webviewRef, editorRef, renderedBody, noteContent, searchMarkers } = props;
|
||||
|
||||
const previousContent = usePrevious(noteContent);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(searchMarkers);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchMarkers) return () => {};
|
||||
|
||||
// If there is a currently active search, it's important to re-search the text as the user
|
||||
// types. However this is slow for performance so we ONLY want it to happen when there is
|
||||
// a search
|
||||
|
||||
// Note that since the CodeMirror component also needs to handle the viewer pane, we need
|
||||
// to check if the rendered body has changed too (it will be changed with a delay after
|
||||
// props.content has been updated).
|
||||
const textChanged = searchMarkers.keywords.length > 0 && (noteContent !== previousContent || renderedBody !== previousRenderedBody);
|
||||
|
||||
if (webviewRef.current && (searchMarkers !== previousSearchMarkers || textChanged)) {
|
||||
webviewRef.current.send('setMarkers', searchMarkers.keywords, searchMarkers.options);
|
||||
|
||||
if (editorRef.current) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7565
|
||||
const debouncedMarkers = debounce(() => {
|
||||
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
|
||||
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}, 50);
|
||||
debouncedMarkers();
|
||||
return () => {
|
||||
debouncedMarkers.clear();
|
||||
};
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
}, [
|
||||
editorRef,
|
||||
webviewRef,
|
||||
searchMarkers,
|
||||
previousSearchMarkers,
|
||||
props.setLocalSearchResultCount,
|
||||
noteContent,
|
||||
previousContent,
|
||||
previousRenderedBody,
|
||||
renderedBody,
|
||||
]);
|
||||
|
||||
};
|
||||
|
||||
export default useEditorSearchHandler;
|
@@ -6,7 +6,7 @@ import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditor
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||
import { CommandValue } from '../../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from '../utils';
|
||||
import { cursorPositionToTextOffset } from '../utils';
|
||||
import useScrollHandler from '../utils/useScrollHandler';
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Toolbar from '../Toolbar';
|
||||
@@ -25,13 +25,13 @@ import { ThemeAppearance } from '@joplin/lib/themes/type';
|
||||
import dialogs from '../../../../dialogs';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
const { clipboard } = require('electron');
|
||||
const debounce = require('debounce');
|
||||
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||
import useStyles from '../utils/useStyles';
|
||||
import useContextMenu from '../utils/useContextMenu';
|
||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return { ...override };
|
||||
@@ -45,10 +45,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousContent = usePrevious(props.content);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
const rootRef = useRef(null);
|
||||
const webviewRef = useRef(null);
|
||||
@@ -247,7 +243,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
if (commands[cmd.name]) {
|
||||
commandOutput = commands[cmd.name](cmd.value);
|
||||
} else if (editorRef.current.supportsCommand(cmd)) {
|
||||
} else if (await editorRef.current.supportsCommand(cmd)) {
|
||||
commandOutput = editorRef.current.execCommandFromJoplin(cmd);
|
||||
} else {
|
||||
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
|
||||
@@ -675,37 +671,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.searchMarkers) return () => {};
|
||||
|
||||
// If there is a currently active search, it's important to re-search the text as the user
|
||||
// types. However this is slow for performance so we ONLY want it to happen when there is
|
||||
// a search
|
||||
|
||||
// Note that since the CodeMirror component also needs to handle the viewer pane, we need
|
||||
// to check if the rendered body has changed too (it will be changed with a delay after
|
||||
// props.content has been updated).
|
||||
const textChanged = props.searchMarkers.keywords.length > 0 && (props.content !== previousContent || renderedBody !== previousRenderedBody);
|
||||
|
||||
if (webviewRef.current && (props.searchMarkers !== previousSearchMarkers || textChanged)) {
|
||||
webviewRef.current.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
if (editorRef.current) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7565
|
||||
const debouncedMarkers = debounce(() => {
|
||||
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}, 50);
|
||||
debouncedMarkers();
|
||||
return () => {
|
||||
debouncedMarkers.clear();
|
||||
};
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
|
||||
useEditorSearchHandler({
|
||||
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||
searchMarkers: props.searchMarkers,
|
||||
webviewRef,
|
||||
editorRef,
|
||||
noteContent: props.content,
|
||||
renderedBody,
|
||||
});
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
@@ -719,11 +692,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
const cellViewerStyle = useMemo(() => {
|
||||
const output = { ...styles.cellViewer };
|
||||
if (!props.visiblePanes.includes('viewer')) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
output.width = 1;
|
||||
output.maxWidth = 1;
|
||||
output.display = 'none';
|
||||
} else if (!props.visiblePanes.includes('editor')) {
|
||||
output.borderLeftStyle = 'none';
|
||||
}
|
||||
|
@@ -12,15 +12,15 @@ import 'codemirror/addon/scroll/annotatescrollbar';
|
||||
import 'codemirror/addon/search/matchesonscrollbar';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
|
||||
import useListIdent from '../utils/useListIdent';
|
||||
import useScrollUtils from '../utils/useScrollUtils';
|
||||
import useCursorUtils from '../utils/useCursorUtils';
|
||||
import useLineSorting from '../utils/useLineSorting';
|
||||
import useEditorSearch from '../utils/useEditorSearch';
|
||||
import useJoplinMode from '../utils/useJoplinMode';
|
||||
import useKeymap from '../utils/useKeymap';
|
||||
import useExternalPlugins from '../utils/useExternalPlugins';
|
||||
import useJoplinCommands from '../utils/useJoplinCommands';
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import useScrollUtils from './utils/useScrollUtils';
|
||||
import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useExternalPlugins from './utils/useExternalPlugins';
|
||||
import useJoplinCommands from './utils/useJoplinCommands';
|
||||
|
||||
import 'codemirror/keymap/emacs';
|
||||
import 'codemirror/keymap/vim';
|
||||
@@ -283,7 +283,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
}
|
||||
}, [pluginOptions, editor]);
|
||||
|
||||
return <div className='codeMirrorEditor' style={props.style} ref={editorParent} />;
|
||||
return <div className='codeMirrorEditor CodeMirror5' style={props.style} ref={editorParent} />;
|
||||
}
|
||||
|
||||
export default forwardRef(Editor);
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
|
||||
import { EditorCommand } from '../../../utils/types';
|
||||
import { EditorCommand } from '../../../../utils/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import { EventName } from '@joplin/lib/eventManager';
|
||||
import normalizeAccelerator from '../../utils/normalizeAccelerator';
|
||||
import { CodeMirrorVersion } from '../../utils/types';
|
||||
|
||||
export default function useKeymap(CodeMirror: any) {
|
||||
|
||||
@@ -28,29 +30,6 @@ export default function useKeymap(CodeMirror: any) {
|
||||
return command.slice(7); // 7 is the length of editor.
|
||||
}
|
||||
|
||||
// CodeMirror and Electron register accelerators slightly different
|
||||
// CodeMirror requires a - between keys while Electron want's a +
|
||||
// CodeMirror doesn't recognize Option (it uses Alt instead)
|
||||
// CodeMirror requires Shift to be first
|
||||
function normalizeAccelerator(accelerator: string) {
|
||||
const command = accelerator.replace(/\+/g, '-').replace('Option', 'Alt');
|
||||
// From here is taken out of codemirror/lib/codemirror.js
|
||||
const parts = command.split(/-(?!$)/);
|
||||
|
||||
let name = parts[parts.length - 1];
|
||||
let alt, ctrl, shift, cmd;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const mod = parts[i];
|
||||
if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } else if (/^a(lt)?$/i.test(mod)) { alt = true; } else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } else if (/^s(hift)?$/i.test(mod)) { shift = true; } else { throw new Error(`Unrecognized modifier name: ${mod}`); }
|
||||
}
|
||||
if (alt) { name = `Alt-${name}`; }
|
||||
if (ctrl) { name = `Ctrl-${name}`; }
|
||||
if (cmd) { name = `Cmd-${name}`; }
|
||||
if (shift) { name = `Shift-${name}`; }
|
||||
return name;
|
||||
// End of code taken from codemirror/lib/codemirror.js
|
||||
}
|
||||
|
||||
// Because there is sometimes a clash between these keybindings and the Joplin window ones
|
||||
// (This specifically can happen with the Ctrl-B and Ctrl-I keybindings when
|
||||
// codemirror is in contenteditable mode)
|
||||
@@ -74,7 +53,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
}
|
||||
|
||||
// CodeMirror and Electron have slightly different formats for defining accelerators
|
||||
const acc = normalizeAccelerator(key.accelerator);
|
||||
const acc = normalizeAccelerator(key.accelerator, CodeMirrorVersion.CodeMirror5);
|
||||
|
||||
CodeMirror.keyMap.default[acc] = command;
|
||||
}
|
||||
@@ -145,6 +124,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
'Alt-Right': 'goLineEnd',
|
||||
'Ctrl-Backspace': 'delGroupBefore',
|
||||
'Ctrl-Delete': 'delGroupAfter',
|
||||
'Ctrl-Enter': 'insertLineAfter',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
@@ -167,6 +147,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
'Alt-Backspace': 'delGroupBefore',
|
||||
'Alt-Delete': 'delGroupAfter',
|
||||
'Cmd-Backspace': 'delWrappedLineLeft',
|
||||
'Cmd-Enter': 'insertLineAfter',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
@@ -26,6 +26,7 @@ import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import useContextMenu from '../utils/useContextMenu';
|
||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import Toolbar from '../Toolbar';
|
||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -303,11 +304,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
const cellViewerStyle = useMemo(() => {
|
||||
const output = { ...styles.cellViewer };
|
||||
if (!props.visiblePanes.includes('viewer')) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
output.width = 1;
|
||||
output.maxWidth = 1;
|
||||
output.display = 'none';
|
||||
} else if (!props.visiblePanes.includes('editor')) {
|
||||
output.borderLeftStyle = 'none';
|
||||
}
|
||||
@@ -338,6 +335,15 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
// }
|
||||
// }, [editorPaneVisible]);
|
||||
|
||||
useEditorSearchHandler({
|
||||
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||
searchMarkers: props.searchMarkers,
|
||||
webviewRef,
|
||||
editorRef,
|
||||
noteContent: props.content,
|
||||
renderedBody,
|
||||
});
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
|
@@ -10,6 +10,8 @@ import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
@@ -32,6 +34,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
onLogMessageRef.current = props.onLogMessage;
|
||||
}, [props.onEvent, props.onLogMessage]);
|
||||
|
||||
useEditorSearch(editor);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return () => {};
|
||||
@@ -95,6 +99,12 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
const editor = createEditor(editorContainerRef.current, editorProps);
|
||||
editor.addStyles({
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
'&.CodeMirror': {
|
||||
height: 'unset',
|
||||
background: 'unset',
|
||||
overflow: 'unset',
|
||||
direction: 'unset',
|
||||
},
|
||||
});
|
||||
setEditor(editor);
|
||||
|
||||
@@ -104,6 +114,26 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
|
||||
}, []);
|
||||
|
||||
const theme = props.settings.themeData;
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const styles = editor.addStyles({
|
||||
'& .cm-search-marker *, & .cm-search-marker': {
|
||||
color: theme.searchMarkerColor,
|
||||
backgroundColor: theme.searchMarkerBackgroundColor,
|
||||
},
|
||||
'& .cm-search-marker-selected *, & .cm-search-marker-selected': {
|
||||
background: `${theme.selectedColor2} !important`,
|
||||
color: `${theme.color2} !important`,
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
styles.remove();
|
||||
};
|
||||
}, [editor, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
editor?.updateSettings(props.settings);
|
||||
}, [props.settings, editor]);
|
||||
@@ -116,6 +146,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
setupVim(editor);
|
||||
}, [editor]);
|
||||
|
||||
useKeymap(editor);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={props.style}
|
||||
|
@@ -125,7 +125,7 @@ const useEditorCommands = (props: Props) => {
|
||||
}
|
||||
},
|
||||
search: () => {
|
||||
editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
return editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
};
|
||||
}, [
|
||||
|
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from 'react';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import normalizeAccelerator from '../../utils/normalizeAccelerator';
|
||||
import { CodeMirrorVersion } from '../../utils/types';
|
||||
|
||||
const useKeymap = (editorControl: CodeMirrorControl) => {
|
||||
useEffect(() => {
|
||||
if (!editorControl) return () => {};
|
||||
|
||||
// Some commands aren't registered with the command service
|
||||
// (e.g. Quit). Don't have CodeMirror handle these.
|
||||
// See gui/KeymapConfig/getLabel.ts.
|
||||
const isCommandRegistered = (commandName: string) => {
|
||||
const commandNames = CommandService.instance().commandNames();
|
||||
return commandNames.includes(commandName);
|
||||
};
|
||||
|
||||
const keymapItemToCodeMirror = (binding: KeymapItem) => {
|
||||
if (!binding.accelerator || !isCommandRegistered(binding.command)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: normalizeAccelerator(
|
||||
binding.accelerator, CodeMirrorVersion.CodeMirror6,
|
||||
),
|
||||
run: () => {
|
||||
void CommandService.instance().execute(binding.command);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const keymapItems = KeymapService.instance().getKeymapItems();
|
||||
const addedKeymap = editorControl.prependKeymap(
|
||||
keymapItems
|
||||
.map(item => keymapItemToCodeMirror(item))
|
||||
.filter(item => !!item),
|
||||
);
|
||||
|
||||
return () => {
|
||||
addedKeymap.remove();
|
||||
};
|
||||
}, [editorControl]);
|
||||
};
|
||||
|
||||
export default useKeymap;
|
@@ -31,6 +31,7 @@ import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
|
||||
import markupRenderOptions from '../../utils/markupRenderOptions';
|
||||
import { DropHandler } from '../../utils/useDropHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useWebViewApi from './utils/useWebViewApi';
|
||||
const md5 = require('md5');
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
@@ -348,6 +349,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useWebViewApi(editor);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor;
|
||||
@@ -377,10 +380,19 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
background-color: ${theme.backgroundColor} !important;
|
||||
}
|
||||
|
||||
.tox .tox-dialog__body-content {
|
||||
.tox .tox-dialog__body-content,
|
||||
.tox .tox-collection__item {
|
||||
color: ${theme.color};
|
||||
}
|
||||
|
||||
.tox .tox-collection--list .tox-collection__item--active {
|
||||
color: ${theme.backgroundColor};
|
||||
}
|
||||
|
||||
.tox .tox-collection__item--state-disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tox .tox-menu {
|
||||
/* Ensures that popover menus (the color swatch menu) has a visible border
|
||||
even in dark mode. */
|
||||
@@ -584,7 +596,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
'h1', 'h2', 'h3', '|',
|
||||
'hr', '|',
|
||||
'blockquote', '|',
|
||||
'table', '|',
|
||||
'tableWithHeader', '|',
|
||||
`joplinInsertDateTime${toolbarPluginButtons}`,
|
||||
];
|
||||
|
||||
@@ -674,6 +686,22 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
editor.ui.registry.addMenuButton('tableWithHeader', {
|
||||
icon: 'table',
|
||||
tooltip: 'Table',
|
||||
fetch: (callback) => {
|
||||
callback([
|
||||
{
|
||||
type: 'fancymenuitem',
|
||||
fancytype: 'inserttable',
|
||||
onAction: (data) => {
|
||||
editor.execCommand('mceInsertTable', false, { rows: data.numRows, columns: data.numColumns, options: { headerRows: 1 } });
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
editor.ui.registry.addButton('joplinInsertDateTime', {
|
||||
tooltip: _('Insert time'),
|
||||
icon: 'insert-time',
|
||||
|
@@ -0,0 +1,72 @@
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { useEffect } from 'react';
|
||||
import { Editor } from 'tinymce';
|
||||
|
||||
const useWebViewApi = (editor: Editor) => {
|
||||
useEffect(() => {
|
||||
if (!editor) return ()=>{};
|
||||
|
||||
const scriptElement = document.createElement('script');
|
||||
const channelId = `plugin-post-message-${Math.random()}`;
|
||||
scriptElement.appendChild(document.createTextNode(`
|
||||
window.webviewApi = {
|
||||
postMessage: (contentScriptId, message) => {
|
||||
const channelId = ${JSON.stringify(channelId)};
|
||||
const messageId = Math.random();
|
||||
window.parent.postMessage({
|
||||
channelId,
|
||||
messageId,
|
||||
contentScriptId,
|
||||
message,
|
||||
}, '*');
|
||||
|
||||
const waitForResponse = async () => {
|
||||
while (true) {
|
||||
const messageEvent = await new Promise(resolve => {
|
||||
window.addEventListener('message', event => {
|
||||
resolve(event);
|
||||
}, {once: true});
|
||||
});
|
||||
|
||||
if (messageEvent.source !== window.parent || messageEvent.data.messageId !== messageId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = messageEvent.data;
|
||||
return data.response;
|
||||
}
|
||||
};
|
||||
|
||||
return waitForResponse();
|
||||
},
|
||||
};
|
||||
`));
|
||||
const editorWindow = editor.getWin();
|
||||
editorWindow.document.head.appendChild(scriptElement);
|
||||
|
||||
const onMessageHandler = async (event: MessageEvent) => {
|
||||
if (event.source !== editorWindow || event.data.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScriptId = event.data.contentScriptId;
|
||||
const pluginService = PluginService.instance();
|
||||
const plugin = pluginService.pluginById(
|
||||
pluginService.pluginIdByContentScriptId(contentScriptId),
|
||||
);
|
||||
const result = await plugin.emitContentScriptMessage(contentScriptId, event.data.message);
|
||||
editorWindow.postMessage({
|
||||
messageId: event.data.messageId,
|
||||
response: result,
|
||||
}, '*');
|
||||
};
|
||||
window.addEventListener('message', onMessageHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessageHandler);
|
||||
scriptElement.remove();
|
||||
};
|
||||
}, [editor]);
|
||||
};
|
||||
|
||||
export default useWebViewApi;
|
@@ -69,6 +69,7 @@ function styles_(props: Props) {
|
||||
},
|
||||
toolbarStyle: {
|
||||
marginBottom: 0,
|
||||
minWidth: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@@ -9,7 +9,7 @@ export const declaration: CommandDeclaration = {
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async () => {
|
||||
if (comp.editorRef.current && comp.editorRef.current.supportsCommand('search')) {
|
||||
if (comp.editorRef.current && await comp.editorRef.current.supportsCommand('search')) {
|
||||
comp.editorRef.current.execCommand({ name: 'search' });
|
||||
} else {
|
||||
if (comp.noteSearchBarRef.current) {
|
||||
|
@@ -26,9 +26,11 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
|
||||
export async function formNoteToNote(formNote: FormNote): Promise<any> {
|
||||
return {
|
||||
id: formNote.id,
|
||||
// Should also include parent_id so that the reducer can know in which folder the note should go when saving
|
||||
// Should also include parent_id and deleted_time so that the reducer
|
||||
// can know in which folder the note should go when saving.
|
||||
// https://discourse.joplinapp.org/t/experimental-wysiwyg-editor-in-joplin/6915/57?u=laurent
|
||||
parent_id: formNote.parent_id,
|
||||
deleted_time: formNote.deleted_time,
|
||||
title: formNote.title,
|
||||
body: formNote.body,
|
||||
};
|
||||
|
@@ -4,6 +4,23 @@ import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
import HtmlToMd from '@joplin/lib/HtmlToMd';
|
||||
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||
|
||||
const createTestMarkupConverters = () => {
|
||||
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
|
||||
const conv = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
customCss: '',
|
||||
});
|
||||
return conv.render(markupLanguage, markup, {}, options);
|
||||
};
|
||||
|
||||
const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => {
|
||||
const conv = new HtmlToMd();
|
||||
return conv.parse(html);
|
||||
};
|
||||
|
||||
return { markupToHtml, htmlToMd };
|
||||
};
|
||||
|
||||
describe('resourceHandling', () => {
|
||||
it('should sanitize pasted HTML', async () => {
|
||||
Setting.setConstant('resourceDir', '/home/.config/joplin/resources');
|
||||
@@ -27,18 +44,7 @@ describe('resourceHandling', () => {
|
||||
});
|
||||
|
||||
it('should clean up pasted HTML', async () => {
|
||||
const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => {
|
||||
const conv = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
customCss: '',
|
||||
});
|
||||
return conv.render(markupLanguage, markup, {}, options);
|
||||
};
|
||||
|
||||
const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => {
|
||||
const conv = new HtmlToMd();
|
||||
return conv.parse(html);
|
||||
};
|
||||
const { markupToHtml, htmlToMd } = createTestMarkupConverters();
|
||||
|
||||
const testCases = [
|
||||
['<p style="background-color: red">Hello</p><p style="display: hidden;">World</p>', '<p>Hello</p>\n<p>World</p>\n'],
|
||||
@@ -50,4 +56,11 @@ describe('resourceHandling', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve images pasted from the resource directory', async () => {
|
||||
const { markupToHtml, htmlToMd } = createTestMarkupConverters();
|
||||
|
||||
// All images in the resource directory should be preserved.
|
||||
const html = `<img src="file://${encodeURI(Setting.value('resourceDir'))}/resource.png" alt="test"/>`;
|
||||
expect(await processPastedHtml(html, htmlToMd, markupToHtml)).toBe(html);
|
||||
});
|
||||
});
|
||||
|
@@ -138,17 +138,11 @@ export async function getResourcesFromPasteEvent(event: any) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
|
||||
|
||||
const processImagesInPastedHtml = async (html: string) => {
|
||||
const allImageUrls: string[] = [];
|
||||
const mappedResources: Record<string, string> = {};
|
||||
|
||||
// When copying text from eg. GitHub, the HTML might contain non-breaking
|
||||
// spaces instead of regular spaces. If these non-breaking spaces are
|
||||
// inserted into the TinyMCE editor (using insertContent), they will be
|
||||
// dropped. So here we convert them to regular spaces.
|
||||
// https://stackoverflow.com/a/31790544/561309
|
||||
html = html.replace(/[\u202F\u00A0]/g, ' ');
|
||||
|
||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||
allImageUrls.push(src);
|
||||
});
|
||||
@@ -200,6 +194,19 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
|
||||
|
||||
await Promise.all(downloadImages);
|
||||
|
||||
return htmlUtils.replaceImageUrls(html, (src: string) => mappedResources[src]);
|
||||
};
|
||||
|
||||
export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHandler | null, mdToHtml: MarkupToHtmlHandler | null) {
|
||||
// When copying text from eg. GitHub, the HTML might contain non-breaking
|
||||
// spaces instead of regular spaces. If these non-breaking spaces are
|
||||
// inserted into the TinyMCE editor (using insertContent), they will be
|
||||
// dropped. So here we convert them to regular spaces.
|
||||
// https://stackoverflow.com/a/31790544/561309
|
||||
html = html.replace(/[\u202F\u00A0]/g, ' ');
|
||||
|
||||
html = await processImagesInPastedHtml(html);
|
||||
|
||||
// TinyMCE can accept any type of HTML, including HTML that may not be preserved once saved as
|
||||
// Markdown. For example the content may have a dark background which would be supported by
|
||||
// TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back
|
||||
@@ -209,11 +216,7 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
|
||||
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
|
||||
}
|
||||
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||
return mappedResources[src];
|
||||
}), {
|
||||
allowedFilePrefixes: [Setting.value('resourceDir')],
|
||||
},
|
||||
));
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, {
|
||||
allowedFilePrefixes: [Setting.value('resourceDir')],
|
||||
}));
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ export interface NoteBodyEditorRef {
|
||||
resetScroll(): void;
|
||||
scrollTo(options: ScrollOptions): void;
|
||||
|
||||
supportsCommand(name: string): boolean;
|
||||
supportsCommand(name: string): boolean|Promise<boolean>;
|
||||
execCommand(command: CommandValue): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface FormNote {
|
||||
markup_language: number;
|
||||
user_updated_time: number;
|
||||
encryption_applied: number;
|
||||
deleted_time: number;
|
||||
|
||||
hasChanged: boolean;
|
||||
|
||||
@@ -173,6 +174,7 @@ export function defaultFormNote(): FormNote {
|
||||
return {
|
||||
id: '',
|
||||
parent_id: '',
|
||||
deleted_time: 0,
|
||||
title: '',
|
||||
body: '',
|
||||
is_todo: 0,
|
||||
|
@@ -13,6 +13,7 @@ import Note from '@joplin/lib/models/Note';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
@@ -77,7 +78,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: any) {
|
||||
async function initNoteState(n: NoteEntity) {
|
||||
let originalCss = '';
|
||||
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
@@ -91,6 +92,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
body: n.body,
|
||||
is_todo: n.is_todo,
|
||||
parent_id: n.parent_id,
|
||||
deleted_time: n.deleted_time,
|
||||
bodyWillChangeId: 0,
|
||||
bodyChangeId: 0,
|
||||
markup_language: n.markup_language,
|
||||
|
@@ -22,6 +22,7 @@ import usePrevious from '../hooks/usePrevious';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import { registerGlobalDragEndEvent, unregisterGlobalDragEndEvent } from '../utils/dragAndDrop';
|
||||
|
||||
const commands = [
|
||||
require('./commands/focusElementNoteList'),
|
||||
@@ -64,8 +65,6 @@ const NoteListComponent = (props: Props) => {
|
||||
const noteListRef = useRef(null);
|
||||
const itemListRef = useRef(null);
|
||||
|
||||
let globalDragEndEventRegistered_ = false;
|
||||
|
||||
const style = useMemo(() => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
@@ -129,22 +128,6 @@ const NoteListComponent = (props: Props) => {
|
||||
menu.popup({ window: bridge().window() });
|
||||
}, [props.selectedNoteIds, props.notes, props.dispatch, props.watchedNoteFiles, props.plugins, props.selectedFolderId, props.customCss]);
|
||||
|
||||
const onGlobalDrop_ = () => {
|
||||
unregisterGlobalDragEndEvent_();
|
||||
setDragOverTargetNoteIndex(null);
|
||||
};
|
||||
|
||||
const registerGlobalDragEndEvent_ = () => {
|
||||
if (globalDragEndEventRegistered_) return;
|
||||
globalDragEndEventRegistered_ = true;
|
||||
document.addEventListener('dragend', onGlobalDrop_);
|
||||
};
|
||||
|
||||
const unregisterGlobalDragEndEvent_ = () => {
|
||||
globalDragEndEventRegistered_ = false;
|
||||
document.removeEventListener('dragend', onGlobalDrop_);
|
||||
};
|
||||
|
||||
const dragTargetNoteIndex_ = (event: any) => {
|
||||
return Math.abs(Math.round((event.clientY - itemListRef.current.offsetTop() + itemListRef.current.offsetScroll()) / itemHeight));
|
||||
};
|
||||
@@ -158,7 +141,7 @@ const NoteListComponent = (props: Props) => {
|
||||
event.preventDefault();
|
||||
const newIndex = dragTargetNoteIndex_(event);
|
||||
if (dragOverTargetNoteIndex === newIndex) return;
|
||||
registerGlobalDragEndEvent_();
|
||||
registerGlobalDragEndEvent(() => setDragOverTargetNoteIndex(null));
|
||||
setDragOverTargetNoteIndex(newIndex);
|
||||
}
|
||||
};
|
||||
@@ -185,7 +168,7 @@ const NoteListComponent = (props: Props) => {
|
||||
return;
|
||||
}
|
||||
const dt = event.dataTransfer;
|
||||
unregisterGlobalDragEndEvent_();
|
||||
unregisterGlobalDragEndEvent();
|
||||
setDragOverTargetNoteIndex(null);
|
||||
|
||||
const targetNoteIndex = dragTargetNoteIndex_(event);
|
||||
|
@@ -22,6 +22,8 @@ import * as focusElementNoteList from './commands/focusElementNoteList';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useDragAndDrop from './utils/useDragAndDrop';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { itemIsInTrash } from '@joplin/lib/services/trash';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
const commands = {
|
||||
@@ -74,6 +76,7 @@ const NoteList = (props: Props) => {
|
||||
props.uncompletedTodosOnTop,
|
||||
props.showCompletedTodos,
|
||||
props.notes,
|
||||
props.selectedFolderInTrash,
|
||||
);
|
||||
|
||||
const noteItemStyle = useMemo(() => {
|
||||
@@ -136,6 +139,7 @@ const NoteList = (props: Props) => {
|
||||
props.showCompletedTodos,
|
||||
listRenderer.flow,
|
||||
itemsPerLine,
|
||||
props.selectedFolderInTrash,
|
||||
);
|
||||
|
||||
const previousSelectedNoteIds = usePrevious(props.selectedNoteIds, []);
|
||||
@@ -209,6 +213,7 @@ const NoteList = (props: Props) => {
|
||||
isWatched={props.watchedNoteFiles.includes(note.id)}
|
||||
listRenderer={listRenderer}
|
||||
dispatch={props.dispatch}
|
||||
columns={props.columns}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -264,7 +269,7 @@ const NoteList = (props: Props) => {
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? Folder.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
|
||||
return {
|
||||
@@ -287,6 +292,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
selectedFolderInTrash: itemIsInTrash(selectedFolder),
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
.note-list-item {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-list-item-wrapper {
|
||||
@@ -36,8 +36,12 @@
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item-wrapper.-provisional {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,9 @@ import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
const canManuallySortNotes = (notesParentType: string, noteSortOrder: string) => {
|
||||
const canManuallySortNotes = (notesParentType: string, noteSortOrder: string, selectedFolderInTrash: boolean) => {
|
||||
if (notesParentType !== 'Folder') return false;
|
||||
if (selectedFolderInTrash) return false;
|
||||
|
||||
if (noteSortOrder !== 'order') {
|
||||
const doIt = bridge().showConfirmMessageBox(_('To manually sort the notes, the sort order must be changed to "%s" in the menu "%s" > "%s"', _('Custom order'), _('View'), _('Sort notes by')), {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user