You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-02-01 07:49:31 +02:00
Compare commits
5 Commits
android-v3
...
sharing_bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53bb2d167 | ||
|
|
113b259b81 | ||
|
|
c7e31d1ac9 | ||
|
|
c51b13ca73 | ||
|
|
f5febb18b4 |
17
.env-sample
17
.env-sample
@@ -15,23 +15,6 @@
|
||||
# POSTGRES_PORT=5432
|
||||
# POSTGRES_HOST=localhost
|
||||
|
||||
# =============================================================================
|
||||
# TRANSCRIBE CONFIG EXAMPLE
|
||||
# -----------------------------------------------------------------------------
|
||||
# This service is not required, and it will be ignored by using --profile server
|
||||
# when running docker-compose. If you want to use it, you need to set the
|
||||
# following environment variables.
|
||||
# =============================================================================
|
||||
|
||||
# TRANSCRIBE_API_KEY=secret_string_shared_between_server_and_transcribe
|
||||
# TRANSCRIBE_ENABLED=true
|
||||
|
||||
# QUEUE_DATABASE_NAME=transcribe
|
||||
# QUEUE_DATABASE_USER=transcribe
|
||||
# QUEUE_DATABASE_PASSWORD=transcribe
|
||||
# QUEUE_DATABASE_PORT=5431
|
||||
# HTR_CLI_IMAGES_FOLDER=/home/user/images_storage
|
||||
|
||||
# =============================================================================
|
||||
# DEV CONFIG EXAMPLE
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -11,9 +11,8 @@ QUEUE_RETRY_COUNT=2
|
||||
QUEUE_MAINTENANCE_INTERVAL=30000
|
||||
|
||||
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:0.0.2
|
||||
# Fullpath to images folder e.g.:
|
||||
#HTR_CLI_IMAGES_FOLDER=/home/user/joplin/packages/transcribe/images
|
||||
HTR_CLI_IMAGES_FOLDER=
|
||||
# Fullpath to images folder
|
||||
HTR_CLI_IMAGES_FOLDER=/home/user/joplin/packages/transcribe/images
|
||||
|
||||
QUEUE_DRIVER=pg
|
||||
# QUEUE_DRIVER=sqlite
|
||||
@@ -28,5 +27,4 @@ QUEUE_DRIVER=pg
|
||||
QUEUE_DATABASE_NAME=transcribe
|
||||
QUEUE_DATABASE_USER=transcribe
|
||||
QUEUE_DATABASE_PASSWORD=transcribe
|
||||
QUEUE_DATABASE_PORT=5432
|
||||
QUEUE_DATABASE_HOST=localhost
|
||||
QUEUE_DATABASE_PORT=5432
|
||||
224
.eslintignore
224
.eslintignore
@@ -55,7 +55,6 @@ packages/app-desktop/vendor/lib/
|
||||
packages/app-mobile/packageInfo.js
|
||||
packages/app-mobile/android
|
||||
packages/app-mobile/**/*.bundle.js
|
||||
packages/app-mobile/**/*.bundle.css
|
||||
packages/app-mobile/web/public/pluginAssets/**/*
|
||||
packages/app-mobile/ios
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
@@ -75,7 +74,6 @@ packages/lib/services/database/types.ts
|
||||
packages/lib/vendor/
|
||||
packages/lib/vendor/fountain.min.js
|
||||
packages/lib/welcomeAssets.js
|
||||
packages/editor/*/vendor/
|
||||
packages/plugins/**/api
|
||||
packages/plugins/**/dist
|
||||
packages/server/dist/
|
||||
@@ -98,7 +96,6 @@ packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
packages/app-cli/app/command-cat.js
|
||||
packages/app-cli/app/command-config.js
|
||||
packages/app-cli/app/command-cp.js
|
||||
@@ -136,7 +133,6 @@ packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
packages/app-cli/app/utils/initializeCommandService.js
|
||||
packages/app-cli/app/utils/iterateStdin.js
|
||||
packages/app-cli/app/utils/shimInitCli.js
|
||||
packages/app-cli/app/utils/testUtils.js
|
||||
packages/app-cli/tests/HtmlToMd.js
|
||||
@@ -159,8 +155,6 @@ packages/app-desktop/app.reducer.js
|
||||
packages/app-desktop/app.js
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/convertNoteToMarkdown.test.js
|
||||
packages/app-desktop/commands/convertNoteToMarkdown.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyToClipboard.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
@@ -201,7 +195,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/ConversionNotification/ConversionNotification.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@@ -303,7 +296,6 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getResourceBaseUrl.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
@@ -561,8 +553,6 @@ packages/app-desktop/integration-tests/util/setSettingValue.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
packages/app-desktop/main-html.js
|
||||
packages/app-desktop/main.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
|
||||
@@ -635,21 +625,16 @@ packages/app-mobile/components/CameraView/Camera/index.web.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraView.web.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/PhotoPreview.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/ComboBox.test.js
|
||||
packages/app-mobile/components/ComboBox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
packages/app-mobile/components/DialogManager/PromptDialog.js
|
||||
packages/app-mobile/components/DialogManager/TextInputDialog.js
|
||||
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
|
||||
packages/app-mobile/components/DialogManager/index.js
|
||||
packages/app-mobile/components/DialogManager/types.js
|
||||
@@ -671,56 +656,64 @@ packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NestableFlatList.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.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/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/utils/useEditorMessenger.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/RichTextEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/RichTextEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/WarningBanner.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/testing/createTestEditorProps.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/SafeAreaView.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
@@ -745,7 +738,6 @@ packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginNotification.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
@@ -817,8 +809,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/DocumentScanner/DocumentScanner.js
|
||||
packages/app-mobile/components/screens/DocumentScanner/NotePreview.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note/Note.test.js
|
||||
@@ -857,37 +847,6 @@ packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
packages/app-mobile/components/voiceTyping/RecordingControls.js
|
||||
packages/app-mobile/components/voiceTyping/SpeechToTextBanner.js
|
||||
packages/app-mobile/components/voiceTyping/types.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/applyTemplateToEditor.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/startAutosaveLoop.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/types.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/utils/useEditorMessenger.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/types.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/addPluginAssets.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/afterFullPageRender.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/makeResourceModel.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/types.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useContentScripts.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useEditPopup.test.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useEditPopup.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/convertHtmlToMarkdown.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/types.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/types.js
|
||||
packages/app-mobile/contentScripts/utils/polyfills.js
|
||||
packages/app-mobile/contentScripts/utils/readFileToBase64.js
|
||||
packages/app-mobile/contentScripts/utils/setUpLogger.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
@@ -910,7 +869,7 @@ packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyAssets.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -919,7 +878,6 @@ packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
@@ -947,6 +905,7 @@ packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
@@ -992,67 +951,47 @@ 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/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/referenceLinksStateField.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.test.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.js
|
||||
packages/editor/CodeMirror/extensions/modifierKeyCssExtension.js
|
||||
packages/editor/CodeMirror/extensions/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/addFormattingClasses.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/index.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
|
||||
packages/editor/CodeMirror/testing/createEditorControl.js
|
||||
packages/editor/CodeMirror/testing/createTestEditor.js
|
||||
packages/editor/CodeMirror/testing/findNodesWithName.js
|
||||
packages/editor/CodeMirror/testing/forceFullParse.js
|
||||
packages/editor/CodeMirror/testing/loadLanguages.js
|
||||
packages/editor/CodeMirror/testing/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testing/typeText.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorControl.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/findNodesWithName.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/utils/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
@@ -1071,56 +1010,15 @@ packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
|
||||
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/polyfills.js
|
||||
packages/editor/testing/createEditorSettings.js
|
||||
packages/editor/testing/setUpLogger.js
|
||||
packages/editor/types.js
|
||||
packages/editor/utils/getFileFromPasteEvent.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.spec.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.js
|
||||
@@ -1165,8 +1063,6 @@ packages/lib/JoplinDatabase.js
|
||||
packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
packages/lib/PoorManIntervals.js
|
||||
packages/lib/RotatingLogs.test.js
|
||||
packages/lib/RotatingLogs.js
|
||||
@@ -1184,8 +1080,6 @@ packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/convertHtmlToMarkdown.test.js
|
||||
packages/lib/commands/convertHtmlToMarkdown.js
|
||||
packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
@@ -1202,8 +1096,6 @@ packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
|
||||
@@ -1380,7 +1272,6 @@ packages/lib/services/database/migrations/44.js
|
||||
packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1449,8 +1340,6 @@ packages/lib/services/ocr/OcrDriverBase.js
|
||||
packages/lib/services/ocr/OcrService.test.js
|
||||
packages/lib/services/ocr/OcrService.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTesseract.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTranscribe.test.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTranscribe.js
|
||||
packages/lib/services/ocr/utils/filterOcrText.test.js
|
||||
packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
@@ -1620,7 +1509,6 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
@@ -1771,7 +1659,6 @@ packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
@@ -1803,7 +1690,6 @@ packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
packages/tools/release-server.js
|
||||
packages/tools/release-transcribe.js
|
||||
packages/tools/saveClaConsentRecords.js
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
|
||||
@@ -23,7 +23,6 @@ module.exports = {
|
||||
'FileSystemCreateWritableOptions': 'readonly',
|
||||
'FileSystemHandle': 'readonly',
|
||||
'IDBTransactionMode': 'readonly',
|
||||
'FlatArray': 'readonly',
|
||||
'BigInt': 'readonly',
|
||||
'globalThis': 'readonly',
|
||||
|
||||
|
||||
32
.github/scripts/run_ci.sh
vendored
32
.github/scripts/run_ci.sh
vendored
@@ -7,13 +7,9 @@
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
ROOT_DIR="$SCRIPT_DIR/../.."
|
||||
|
||||
TRANSCRIBE_TAG_PREFIX=transcribe
|
||||
TRANSCRIBE_REPOSITORY=joplin/transcribe
|
||||
|
||||
IS_PULL_REQUEST=0
|
||||
IS_DESKTOP_RELEASE=0
|
||||
IS_SERVER_RELEASE=0
|
||||
IS_TRANSCRIBE_RELEASE=0
|
||||
IS_LINUX=0
|
||||
IS_MACOS=0
|
||||
|
||||
@@ -27,10 +23,6 @@ if [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
IS_SERVER_RELEASE=1
|
||||
fi
|
||||
|
||||
if [[ $GIT_TAG_NAME = $TRANSCRIBE_TAG_PREFIX-* ]]; then
|
||||
IS_TRANSCRIBE_RELEASE=1
|
||||
fi
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
IS_DESKTOP_RELEASE=1
|
||||
fi
|
||||
@@ -49,17 +41,15 @@ DOCKER_IMAGE_PLATFORM="linux/amd64"
|
||||
# a release
|
||||
RUN_TESTS=0
|
||||
|
||||
if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ] && [ "$IS_TRANSCRIBE_RELEASE" = 0 ]; then
|
||||
if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ]; then
|
||||
RUN_TESTS=1
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
if [ "$IS_SERVER_RELEASE" == "0" ] && [ "$IS_TRANSCRIBE_RELEASE" == "0" ]; then
|
||||
# We exit now because nothing works properly with the ARM64 architecture.
|
||||
# We only proceed if building the server image.
|
||||
echo "Running on ARM64 and not trying to build server image - early exit"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ] && [ "$IS_SERVER_RELEASE" == "0" ]; then
|
||||
# We exit now because nothing works properly with the ARM64 architecture.
|
||||
# We only proceed if building the server image.
|
||||
echo "Running on ARM64 and not trying to build server image - early exit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
@@ -90,14 +80,12 @@ echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
|
||||
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
|
||||
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
|
||||
echo "TRANSCRIBE_TAG_PREFIX=$TRANSCRIBE_TAG_PREFIX"
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
|
||||
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
|
||||
echo "IS_DESKTOP_RELEASE=$IS_DESKTOP_RELEASE"
|
||||
echo "IS_SERVER_RELEASE=$IS_SERVER_RELEASE"
|
||||
echo "IS_TRANSCRIBE_RELEASE=$IS_TRANSCRIBE_RELEASE"
|
||||
echo "RUN_TESTS=$RUN_TESTS"
|
||||
echo "IS_LINUX=$IS_LINUX"
|
||||
echo "IS_MACOS=$IS_MACOS"
|
||||
@@ -313,13 +301,9 @@ if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
|
||||
USE_HARD_LINKS=false yarn dist
|
||||
fi
|
||||
elif [[ $IS_LINUX = 1 ]] && [ "$IS_SERVER_RELEASE" == "1" ]; then
|
||||
echo "Step: Building Joplin Server Docker Image..."
|
||||
echo "Step: Building Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
elif [[ $IS_LINUX = 1 ]] && [ "$IS_TRANSCRIBE_RELEASE" == "1" ]; then
|
||||
echo "Step: Building Joplin Transcribe Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn buildServerDocker --docker-file Dockerfile.transcribe --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $TRANSCRIBE_REPOSITORY
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
else
|
||||
echo "Step: Building but *not* publishing desktop application..."
|
||||
|
||||
|
||||
5
.github/workflows/github-actions-main.yml
vendored
5
.github/workflows/github-actions-main.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
uses: ./.github/workflows/shared/setup-build-environment
|
||||
|
||||
- name: Install Docker Engine
|
||||
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get install -y apt-transport-https
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
# a pull request it will fail because the PR doesn't have access to
|
||||
# secrets
|
||||
- uses: docker/login-action@v3
|
||||
if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/transcribe-v'))
|
||||
if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -140,7 +141,7 @@ jobs:
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
yarn install
|
||||
yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
|
||||
|
||||
# Basic test to ensure that the created build is valid. It should exit with
|
||||
# code 0 if it works.
|
||||
|
||||
222
.gitignore
vendored
222
.gitignore
vendored
@@ -71,7 +71,6 @@ packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
packages/app-cli/app/command-cat.js
|
||||
packages/app-cli/app/command-config.js
|
||||
packages/app-cli/app/command-cp.js
|
||||
@@ -109,7 +108,6 @@ packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
packages/app-cli/app/utils/initializeCommandService.js
|
||||
packages/app-cli/app/utils/iterateStdin.js
|
||||
packages/app-cli/app/utils/shimInitCli.js
|
||||
packages/app-cli/app/utils/testUtils.js
|
||||
packages/app-cli/tests/HtmlToMd.js
|
||||
@@ -132,8 +130,6 @@ packages/app-desktop/app.reducer.js
|
||||
packages/app-desktop/app.js
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/convertNoteToMarkdown.test.js
|
||||
packages/app-desktop/commands/convertNoteToMarkdown.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyToClipboard.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
@@ -174,7 +170,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/ConversionNotification/ConversionNotification.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
@@ -276,7 +271,6 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getResourceBaseUrl.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
@@ -534,8 +528,6 @@ packages/app-desktop/integration-tests/util/setSettingValue.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
packages/app-desktop/main-html.js
|
||||
packages/app-desktop/main.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
|
||||
@@ -608,21 +600,16 @@ packages/app-mobile/components/CameraView/Camera/index.web.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraView.web.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/PhotoPreview.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/ComboBox.test.js
|
||||
packages/app-mobile/components/ComboBox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
packages/app-mobile/components/DialogManager/PromptDialog.js
|
||||
packages/app-mobile/components/DialogManager/TextInputDialog.js
|
||||
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
|
||||
packages/app-mobile/components/DialogManager/index.js
|
||||
packages/app-mobile/components/DialogManager/types.js
|
||||
@@ -644,56 +631,64 @@ packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NestableFlatList.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.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/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/utils/useEditorMessenger.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/RichTextEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/RichTextEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/WarningBanner.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/testing/createTestEditorProps.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/SafeAreaView.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
@@ -718,7 +713,6 @@ packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginNotification.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
@@ -790,8 +784,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/DocumentScanner/DocumentScanner.js
|
||||
packages/app-mobile/components/screens/DocumentScanner/NotePreview.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note/Note.test.js
|
||||
@@ -830,37 +822,6 @@ packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
packages/app-mobile/components/voiceTyping/RecordingControls.js
|
||||
packages/app-mobile/components/voiceTyping/SpeechToTextBanner.js
|
||||
packages/app-mobile/components/voiceTyping/types.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/applyTemplateToEditor.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/startAutosaveLoop.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/types.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/contentScript/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/imageEditorBundle/utils/useEditorMessenger.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/contentScript.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/types.js
|
||||
packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/types.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/addPluginAssets.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/afterFullPageRender.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/makeResourceModel.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/types.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useContentScripts.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useEditPopup.test.js
|
||||
packages/app-mobile/contentScripts/rendererBundle/utils/useEditPopup.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/convertHtmlToMarkdown.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/contentScript/index.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/types.js
|
||||
packages/app-mobile/contentScripts/richTextEditorBundle/useWebViewSetup.js
|
||||
packages/app-mobile/contentScripts/types.js
|
||||
packages/app-mobile/contentScripts/utils/polyfills.js
|
||||
packages/app-mobile/contentScripts/utils/readFileToBase64.js
|
||||
packages/app-mobile/contentScripts/utils/setUpLogger.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
@@ -883,7 +844,7 @@ packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyAssets.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -892,7 +853,6 @@ packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
@@ -920,6 +880,7 @@ packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
@@ -965,67 +926,47 @@ 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/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/editorCommands/markdownCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/referenceLinksStateField.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.test.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.js
|
||||
packages/editor/CodeMirror/extensions/modifierKeyCssExtension.js
|
||||
packages/editor/CodeMirror/extensions/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/addFormattingClasses.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
||||
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/index.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
|
||||
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
|
||||
packages/editor/CodeMirror/testing/createEditorControl.js
|
||||
packages/editor/CodeMirror/testing/createTestEditor.js
|
||||
packages/editor/CodeMirror/testing/findNodesWithName.js
|
||||
packages/editor/CodeMirror/testing/forceFullParse.js
|
||||
packages/editor/CodeMirror/testing/loadLanguages.js
|
||||
packages/editor/CodeMirror/testing/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testing/typeText.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorControl.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/findNodesWithName.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/utils/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
@@ -1044,56 +985,15 @@ packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
|
||||
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/polyfills.js
|
||||
packages/editor/testing/createEditorSettings.js
|
||||
packages/editor/testing/setUpLogger.js
|
||||
packages/editor/types.js
|
||||
packages/editor/utils/getFileFromPasteEvent.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.spec.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.js
|
||||
@@ -1138,8 +1038,6 @@ packages/lib/JoplinDatabase.js
|
||||
packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
packages/lib/PoorManIntervals.js
|
||||
packages/lib/RotatingLogs.test.js
|
||||
packages/lib/RotatingLogs.js
|
||||
@@ -1157,8 +1055,6 @@ packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/convertHtmlToMarkdown.test.js
|
||||
packages/lib/commands/convertHtmlToMarkdown.js
|
||||
packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
@@ -1175,8 +1071,6 @@ packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
|
||||
@@ -1353,7 +1247,6 @@ packages/lib/services/database/migrations/44.js
|
||||
packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1422,8 +1315,6 @@ packages/lib/services/ocr/OcrDriverBase.js
|
||||
packages/lib/services/ocr/OcrService.test.js
|
||||
packages/lib/services/ocr/OcrService.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTesseract.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTranscribe.test.js
|
||||
packages/lib/services/ocr/drivers/OcrDriverTranscribe.js
|
||||
packages/lib/services/ocr/utils/filterOcrText.test.js
|
||||
packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
@@ -1593,7 +1484,6 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
@@ -1744,7 +1634,6 @@ packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
@@ -1776,7 +1665,6 @@ packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
packages/tools/release-server.js
|
||||
packages/tools/release-transcribe.js
|
||||
packages/tools/saveClaConsentRecords.js
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
|
||||
@@ -219,7 +219,10 @@
|
||||
$('.feature-description-' + featureId).toggle(200);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
const setHostingType = (type) => {
|
||||
const other = type === 'managed' ? 'self' : 'managed';
|
||||
$('.toggle-button-' + type).addClass('active');
|
||||
@@ -241,7 +244,6 @@
|
||||
setHostingType('self');
|
||||
});
|
||||
|
||||
const initialHostingType = urlQuery.get('hosting') ? urlQuery.get('hosting') : 'managed';
|
||||
setHostingType(initialHostingType);
|
||||
setHostingType('managed');
|
||||
</script>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY .yarn/plugins ./.yarn/plugins
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
COPY .yarn/patches ./.yarn/patches
|
||||
COPY package.json .
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
"version": "latest",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"yarn": "1.22.19",
|
||||
"yarn": "latest",
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "23.10.0",
|
||||
"nodejs": "23.8.0",
|
||||
"pkg-config": "latest",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
"version": "",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"python": "3.13.2",
|
||||
"python": "3.13.1",
|
||||
"bat": "latest",
|
||||
"electron": {
|
||||
"version": "latest",
|
||||
|
||||
@@ -17,21 +17,11 @@
|
||||
|
||||
version: '3'
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
transcribe-network:
|
||||
shared-network:
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
profiles:
|
||||
- full
|
||||
- server
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app-network
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
@@ -41,17 +31,10 @@ services:
|
||||
- POSTGRES_DB=${POSTGRES_DATABASE}
|
||||
app:
|
||||
image: joplin/server:latest
|
||||
profiles:
|
||||
- full
|
||||
- server
|
||||
depends_on:
|
||||
- db
|
||||
- transcribe
|
||||
ports:
|
||||
- "22300:22300"
|
||||
networks:
|
||||
- app-network
|
||||
- shared-network
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_PORT=22300
|
||||
@@ -62,48 +45,3 @@ services:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT}
|
||||
- POSTGRES_HOST=db
|
||||
- TRANSCRIBE_API_KEY=${TRANSCRIBE_API_KEY}
|
||||
- TRANSCRIBE_BASE_URL=http://transcribe:4567
|
||||
- TRANSCRIBE_ENABLED=${TRANSCRIBE_ENABLED}
|
||||
transcribe-db:
|
||||
image: postgres:16
|
||||
profiles:
|
||||
- full
|
||||
volumes:
|
||||
- ./data/transcribe-postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
- transcribe-network
|
||||
ports:
|
||||
- "${QUEUE_DATABASE_PORT}:5432"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${QUEUE_DATABASE_PASSWORD}
|
||||
- POSTGRES_USER=${QUEUE_DATABASE_USER}
|
||||
- POSTGRES_DB=${QUEUE_DATABASE_NAME}
|
||||
command: -p ${QUEUE_DATABASE_PORT}
|
||||
transcribe:
|
||||
image: joplin/transcribe:latest
|
||||
profiles:
|
||||
- full
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HTR_CLI_IMAGES_FOLDER}:/app/packages/transcribe/images
|
||||
depends_on:
|
||||
- transcribe-db
|
||||
ports:
|
||||
- "4567:4567"
|
||||
networks:
|
||||
- transcribe-network
|
||||
- shared-network
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_PORT=4567
|
||||
- DB_CLIENT=pg
|
||||
- QUEUE_DATABASE_NAME=${QUEUE_DATABASE_NAME}
|
||||
- QUEUE_DATABASE_USER=${QUEUE_DATABASE_USER}
|
||||
- QUEUE_DATABASE_PASSWORD=${QUEUE_DATABASE_PASSWORD}
|
||||
- QUEUE_DATABASE_PORT=${QUEUE_DATABASE_PORT}
|
||||
- QUEUE_DATABASE_HOST=transcribe-db
|
||||
- API_KEY=${TRANSCRIBE_API_KEY}
|
||||
- HTR_CLI_IMAGES_FOLDER=${HTR_CLI_IMAGES_FOLDER}
|
||||
|
||||
|
||||
25
package.json
25
package.json
@@ -51,8 +51,6 @@
|
||||
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
|
||||
"releasePluginRepoCli": "node packages/tools/release-plugin-repo-cli.js",
|
||||
"releaseServer": "node packages/tools/release-server.js",
|
||||
"releaseTranscribe": "node packages/tools/release-transcribe.js",
|
||||
"saveClaConsentRecords": "node packages/tools/saveClaConsentRecords.js",
|
||||
"setupNewRelease": "node ./packages/tools/setupNewRelease",
|
||||
"spellcheck": "node packages/tools/spellcheck.js",
|
||||
"tagServerLatest": "node packages/tools/tagServerLatest.js",
|
||||
@@ -74,35 +72,35 @@
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-promise": "6.2.0",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.5.2",
|
||||
"lint-staged": "15.5.0",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "11.2.0",
|
||||
"nodemon": "3.1.10"
|
||||
"node-gyp": "9.4.1",
|
||||
"nodemon": "3.1.9"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"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",
|
||||
@@ -117,7 +115,6 @@
|
||||
"pdfjs-dist@2.16.105": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/",
|
||||
"node-gyp@npm:^9.0.0": "11.2.0"
|
||||
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Tag from '@joplin/lib/models/Tag';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import { reg } from '@joplin/lib/registry.js';
|
||||
import { dirname, fileExtension } from '@joplin/lib/path-utils';
|
||||
import { splitCommandString } from '@joplin/utils';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { pathExists, readFile, readdirSync } from 'fs-extra';
|
||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
@@ -18,6 +19,7 @@ import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const Cache = require('@joplin/lib/Cache');
|
||||
const { splitCommandBatch } = require('@joplin/lib/string-utils');
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -379,6 +381,22 @@ class Application extends BaseApplication {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async commandList(argv: string[]) {
|
||||
if (argv.length && argv[0] === 'batch') {
|
||||
const commands = [];
|
||||
const commandLines = splitCommandBatch(await readFile(argv[1], 'utf-8'));
|
||||
|
||||
for (const commandLine of commandLines) {
|
||||
if (!commandLine.trim()) continue;
|
||||
const splitted = 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.
|
||||
public checkIfKeychainEnabled(argv: string[]) {
|
||||
@@ -415,10 +433,15 @@ class Application extends BaseApplication {
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
|
||||
|
||||
await this.applySettingsSideEffects();
|
||||
await this.refreshCurrentFolder();
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
const commands = await this.commandList(argv);
|
||||
for (const command of commands) {
|
||||
await this.execCommand(command);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.error(error);
|
||||
|
||||
19
packages/app-cli/app/command-batch.js
Normal file
19
packages/app-cli/app/command-batch.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const BaseCommand = require('./base-command').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
usage() {
|
||||
return 'batch <file-path>';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Runs the commands contained in the text file. There should be one command per line.');
|
||||
}
|
||||
|
||||
async action() {
|
||||
// Implementation is in app.js::commandList()
|
||||
throw new Error('No implemented');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { splitCommandBatch } from '@joplin/lib/string-utils';
|
||||
import BaseCommand from './base-command';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { splitCommandString } from '@joplin/utils';
|
||||
import iterateStdin from './utils/iterateStdin';
|
||||
import { readFile } from 'fs-extra';
|
||||
import app from './app';
|
||||
|
||||
interface Options {
|
||||
'file-path': string;
|
||||
options: {
|
||||
'continue-on-failure': boolean;
|
||||
};
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
return 'batch <file-path>';
|
||||
}
|
||||
|
||||
public options() {
|
||||
return [
|
||||
// These are present mostly for testing purposes
|
||||
['--continue-on-failure', 'Continue running commands when one command in the batch fails.'],
|
||||
];
|
||||
}
|
||||
|
||||
public description() {
|
||||
return _('Runs the commands contained in the text file. There should be one command per line.');
|
||||
}
|
||||
|
||||
private streamCommands_ = async function*(filePath: string) {
|
||||
const processLines = function*(lines: string) {
|
||||
const commandLines = splitCommandBatch(lines);
|
||||
|
||||
for (const command of commandLines) {
|
||||
if (!command.trim()) continue;
|
||||
yield splitCommandString(command.trim());
|
||||
}
|
||||
};
|
||||
|
||||
if (filePath === '-') { // stdin
|
||||
// Iterating over standard input conflicts with the CLI app's GUI.
|
||||
if (app().hasGui()) {
|
||||
throw new Error(_('Reading commands from standard input is only available in CLI mode.'));
|
||||
}
|
||||
|
||||
for await (const lines of iterateStdin('command> ')) {
|
||||
yield* processLines(lines);
|
||||
}
|
||||
} else {
|
||||
const data = await readFile(filePath, 'utf-8');
|
||||
yield* processLines(data);
|
||||
}
|
||||
};
|
||||
|
||||
public async action(options: Options) {
|
||||
let lastError;
|
||||
for await (const command of this.streamCommands_(options['file-path'])) {
|
||||
try {
|
||||
await app().refreshCurrentFolder();
|
||||
await app().execCommand(command);
|
||||
} catch (error) {
|
||||
if (options.options['continue-on-failure']) {
|
||||
app().stdout(error.message);
|
||||
lastError = error;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
@@ -14,25 +14,17 @@ class Command extends BaseCommand {
|
||||
return `${_('Start, stop or check the API server. To specify on which port it should run, set the api.port config variable. Commands are (%s).', ['start', 'stop', 'status'].join('|'))} This is an experimental feature - use at your own risks! It is recommended that the server runs off its own separate profile so that no two CLI instances access that profile at the same time. Use --profile to specify the profile path.`;
|
||||
}
|
||||
|
||||
options() {
|
||||
return [
|
||||
['--exit-early', 'Allow the command to exit while the server is still running. The server will still stop when the app exits. Valid only for the `start` subcommand.'],
|
||||
['--quiet', 'Log less information to the console. More verbose logs will still be available through log-clipper.txt.'],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
const command = args.command;
|
||||
|
||||
const ClipperServer = require('@joplin/lib/ClipperServer').default;
|
||||
ClipperServer.instance().initialize();
|
||||
const stdoutFn = (...s) => this.stdout(s.join(' '));
|
||||
const ignoreOutputFn = ()=>{};
|
||||
const clipperLogger = new Logger();
|
||||
clipperLogger.addTarget('file', { path: `${Setting.value('profileDir')}/log-clipper.txt` });
|
||||
clipperLogger.addTarget('console', { console: {
|
||||
info: args.options.quiet ? ignoreOutputFn : stdoutFn,
|
||||
warn: args.options.quiet ? ignoreOutputFn : stdoutFn,
|
||||
info: stdoutFn,
|
||||
warn: stdoutFn,
|
||||
error: stdoutFn,
|
||||
} });
|
||||
ClipperServer.instance().setDispatch(() => {});
|
||||
@@ -46,11 +38,7 @@ class Command extends BaseCommand {
|
||||
this.stdout(_('Server is already running on port %d', runningOnPort));
|
||||
} else {
|
||||
await shim.fsDriver().writeFile(pidPath, process.pid.toString(), 'utf-8');
|
||||
const promise = ClipperServer.instance().start();
|
||||
|
||||
if (!args.options['exit-early']) {
|
||||
await promise; // Never exit
|
||||
}
|
||||
await ClipperServer.instance().start(); // Never exit
|
||||
}
|
||||
} else if (command === 'status') {
|
||||
this.stdout(runningOnPort ? _('Server is running on port %d', runningOnPort) : _('Server is not running.'));
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { createInterface } from 'readline/promises';
|
||||
|
||||
const iterateStdin = async function*(prompt: string) {
|
||||
let nextLineListeners: (()=> void)[] = [];
|
||||
const dispatchAllListeners = () => {
|
||||
const listeners = nextLineListeners;
|
||||
nextLineListeners = [];
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
};
|
||||
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
rl.setPrompt(prompt);
|
||||
|
||||
let buffer: string[] = [];
|
||||
rl.on('line', (line) => {
|
||||
buffer.push(line);
|
||||
dispatchAllListeners();
|
||||
});
|
||||
|
||||
let done = false;
|
||||
rl.on('close', () => {
|
||||
done = true;
|
||||
dispatchAllListeners();
|
||||
});
|
||||
|
||||
const readNextLines = () => {
|
||||
return new Promise<string|null>(resolve => {
|
||||
if (done) {
|
||||
resolve(null);
|
||||
} else if (buffer.length > 0) {
|
||||
resolve(buffer.join('\n'));
|
||||
buffer = [];
|
||||
} else {
|
||||
nextLineListeners.push(() => {
|
||||
resolve(buffer.join('\n'));
|
||||
buffer = [];
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
while (!done) {
|
||||
rl.prompt();
|
||||
const lines = await readNextLines();
|
||||
yield lines;
|
||||
}
|
||||
};
|
||||
|
||||
export default iterateStdin;
|
||||
@@ -9,7 +9,7 @@ const shimInitCli = (options: ShimInitOptions) => {
|
||||
|
||||
shim.showMessageBox = async (message: string, options: ShowMessageBoxOptions) => {
|
||||
const gui = app()?.gui();
|
||||
let answers = options.buttons ?? [_('OK'), _('Cancel')];
|
||||
let answers = options.buttons ?? [_('Ok'), _('Cancel')];
|
||||
|
||||
if (options.type === 'error' || options.type === 'info') {
|
||||
answers = [];
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.1",
|
||||
"sharp": "0.33.5",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -72,12 +72,12 @@
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.4",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.100",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.19.86",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import MarkupToHtml from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult, MarkupLanguage } from '@joplin/renderer/types';
|
||||
import MarkupToHtml, { MarkupLanguage } from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult } from '@joplin/renderer/types';
|
||||
|
||||
describe('MarkupToHtml', () => {
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<p>A task list created by the TipTap editor:</p>
|
||||
<ul data-type="taskList">
|
||||
<li><label contenteditable="false"><input type="checkbox"><span></span></label>
|
||||
<div>
|
||||
<p>Testing...</p>
|
||||
</div>
|
||||
</li>
|
||||
<li><label contenteditable="false"><input type="checkbox"><span></span></label>
|
||||
<div>
|
||||
<p>testing</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1,5 +0,0 @@
|
||||
A task list created by the TipTap editor:
|
||||
|
||||
- [ ] Testing...
|
||||
|
||||
- [ ] testing
|
||||
@@ -1,26 +0,0 @@
|
||||
<p>List 1:</p>
|
||||
<ul>
|
||||
<li><label><input type="checkbox"/>This</label></li>
|
||||
<li><label><input type="checkbox" checked/>is a test.</label></li>
|
||||
</ul>
|
||||
<p>List 2:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="checkbox" id="checkbox-1"/><label for="checkbox-1">This</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" checked id="checkbox-2"/><label for="checkbox-2">is another test.</label>
|
||||
</li>
|
||||
</ul>
|
||||
<p>List 3:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="checkbox" id="checkbox-a1"/><label for="checkbox-a1">This</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" checked id="checkbox-a2"/><label for="checkbox-a2">is another test.</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" checked id="checkbox-a3"/><label for="checkbox-a3"></label>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1,15 +0,0 @@
|
||||
List 1:
|
||||
|
||||
- [ ] This
|
||||
- [x] is a test.
|
||||
|
||||
List 2:
|
||||
|
||||
- [ ] This
|
||||
- [x] is another test.
|
||||
|
||||
List 3:
|
||||
|
||||
- [ ] This
|
||||
- [x] is another test.
|
||||
- [x]
|
||||
@@ -1,7 +1,7 @@
|
||||
<ul class="joplin-checklist" data-is-checklist="1">
|
||||
<ul class="joplin-checklist">
|
||||
<li>Not checked</li>
|
||||
<li class="checked">Checked!!
|
||||
<ul class="joplin-checklist" data-is-checklist="1">
|
||||
<ul class="joplin-checklist">
|
||||
<li class="checked">Indented, with <strong>bold</strong></li>
|
||||
<li>Indented, not checked</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<span class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></span>
|
||||
<span class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></span>
|
||||
<span class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class="jop-noMdConv"/" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class="jop-noMdConv"/" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></span>
|
||||
	"/></div>
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
aliases:
|
||||
- An Obsidian-style note
|
||||
---
|
||||
|
||||
This is a note with no `title` field in the YAML Frontmatter.
|
||||
Joplin should be smart enough to pull the title from the filename in such cases.
|
||||
@@ -8,7 +8,7 @@ import { FileLocker } from '@joplin/utils/fs';
|
||||
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
|
||||
import bridge from './bridge';
|
||||
import * as url from 'url';
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -55,16 +55,11 @@ import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetche
|
||||
import { parseNotesParent } from '@joplin/lib/reducer';
|
||||
import OcrService from '@joplin/lib/services/ocr/OcrService';
|
||||
import OcrDriverTesseract from '@joplin/lib/services/ocr/drivers/OcrDriverTesseract';
|
||||
import OcrDriverTranscribe from '@joplin/lib/services/ocr/drivers/OcrDriverTranscribe';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
import OcrDriverBase from '@joplin/lib/services/ocr/OcrDriverBase';
|
||||
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
||||
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
@@ -72,8 +67,6 @@ const pluginClasses = [
|
||||
|
||||
const appDefaultState = createAppDefaultState(resourceEditWatcherDefaultState);
|
||||
|
||||
type StartupTask = { label: string; task: ()=> void|Promise<void> };
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -355,19 +348,16 @@ class Application extends BaseApplication {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const Tesseract = (window as any).Tesseract;
|
||||
|
||||
const drivers: OcrDriverBase[] = [];
|
||||
drivers.push(new OcrDriverTesseract(
|
||||
const driver = new OcrDriverTesseract(
|
||||
{ createWorker: Tesseract.createWorker },
|
||||
{
|
||||
workerPath: `${bridge().buildDir()}/tesseract.js/worker.min.js`,
|
||||
corePath: `${bridge().buildDir()}/tesseract.js-core`,
|
||||
languageDataPath: Setting.value('ocr.languageDataPath') || null,
|
||||
},
|
||||
));
|
||||
);
|
||||
|
||||
drivers.push(new OcrDriverTranscribe());
|
||||
|
||||
this.ocrService_ = new OcrService(drivers);
|
||||
this.ocrService_ = new OcrService(driver);
|
||||
}
|
||||
|
||||
void this.ocrService_.runInBackground();
|
||||
@@ -421,53 +411,56 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
|
||||
private buildStartupTasks_() {
|
||||
const tasks: StartupTask[] = [];
|
||||
const addTask = (label: string, task: StartupTask['task']) => {
|
||||
tasks.push({ label, task });
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async start(argv: string[], startOptions: StartOptions = null): Promise<any> {
|
||||
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
|
||||
// insert an extra argument so that they can be processed in a consistent way everywhere.
|
||||
if (!bridge().electronIsDev()) argv.splice(1, 0, '.');
|
||||
|
||||
addTask('app/set up extra debug logging', () => {
|
||||
reg.logger().info('app.start: doing regular boot');
|
||||
const dir: string = Setting.value('profileDir');
|
||||
argv = await super.start(argv, startOptions);
|
||||
|
||||
syncDebugLog.enabled = false;
|
||||
await this.setupIntegrationTestUtils();
|
||||
|
||||
if (dir.endsWith('dev-desktop-2')) {
|
||||
syncDebugLog.addTarget(TargetType.File, {
|
||||
path: `${homedir()}/synclog.txt`,
|
||||
});
|
||||
syncDebugLog.enabled = true;
|
||||
syncDebugLog.info(`Profile dir: ${dir}`);
|
||||
}
|
||||
});
|
||||
bridge().setLogFilePath(Logger.globalLogger.logFilePath());
|
||||
|
||||
addTask('app/set up registry', () => {
|
||||
reg.setDispatch(this.dispatch.bind(this));
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
|
||||
});
|
||||
await this.applySettingsSideEffects();
|
||||
|
||||
addTask('app/set up auto updater', () => {
|
||||
this.setupAutoUpdaterService();
|
||||
});
|
||||
|
||||
addTask('app/set up AlarmService', () => {
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
});
|
||||
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
addTask('app/openDevTools', () => {
|
||||
bridge().openDevTools();
|
||||
});
|
||||
if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) {
|
||||
reg.logger().info('app.start: doing upgradeSyncTarget action');
|
||||
bridge().mainWindow().show();
|
||||
return { action: 'upgradeSyncTarget' };
|
||||
}
|
||||
|
||||
addTask('app/set up custom protocol handler', async () => {
|
||||
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
|
||||
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
|
||||
});
|
||||
reg.logger().info('app.start: doing regular boot');
|
||||
|
||||
const dir: string = Setting.value('profileDir');
|
||||
|
||||
syncDebugLog.enabled = false;
|
||||
|
||||
if (dir.endsWith('dev-desktop-2')) {
|
||||
syncDebugLog.addTarget(TargetType.File, {
|
||||
path: `${homedir()}/synclog.txt`,
|
||||
});
|
||||
syncDebugLog.enabled = true;
|
||||
syncDebugLog.info(`Profile dir: ${dir}`);
|
||||
}
|
||||
|
||||
this.setupAutoUpdaterService();
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
reg.setDispatch(this.dispatch.bind(this));
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
|
||||
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
bridge().openDevTools();
|
||||
}
|
||||
|
||||
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
|
||||
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('tempDir'));
|
||||
// For now, this doesn't seem necessary:
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('profileDir'));
|
||||
@@ -475,52 +468,44 @@ class Application extends BaseApplication {
|
||||
// handler, and, as such, it may make sense to also limit permissions of
|
||||
// allowed pages with a Content Security Policy.
|
||||
|
||||
addTask('app/initialize PluginManager, redux, CommandService, and KeymapService', async () => {
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
PluginManager.instance().setLogger(reg.logger());
|
||||
PluginManager.instance().register(pluginClasses);
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
PluginManager.instance().setLogger(reg.logger());
|
||||
PluginManager.instance().register(pluginClasses);
|
||||
|
||||
this.initRedux();
|
||||
this.initRedux();
|
||||
|
||||
initializeCommandService(this.store(), Setting.value('env') === 'dev');
|
||||
PerFolderSortOrderService.initialize();
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
// We only add the commands that appear in the menu because only
|
||||
// those can have a shortcut associated with them.
|
||||
keymapService.initialize(menuCommandNames());
|
||||
initializeCommandService(this.store(), Setting.value('env') === 'dev');
|
||||
|
||||
try {
|
||||
await keymapService.loadCustomKeymap(`${Setting.value('profileDir')}/keymap-desktop.json`);
|
||||
} catch (error) {
|
||||
reg.logger().error(error);
|
||||
}
|
||||
const keymapService = KeymapService.instance();
|
||||
// We only add the commands that appear in the menu because only
|
||||
// those can have a shortcut associated with them.
|
||||
keymapService.initialize(menuCommandNames());
|
||||
|
||||
try {
|
||||
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
|
||||
} catch (error) {
|
||||
reg.logger().error(error);
|
||||
}
|
||||
|
||||
// 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.dispatchUpdateAll();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
await refreshFolders((action: any) => this.dispatch(action), '');
|
||||
|
||||
const tags = await Tag.allWithNotes();
|
||||
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
items: tags,
|
||||
});
|
||||
|
||||
addTask('app/initialize PerFolderSortOrderService', () => {
|
||||
PerFolderSortOrderService.initialize();
|
||||
});
|
||||
|
||||
addTask('app/dispatch initial settings', () => {
|
||||
// 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.dispatchUpdateAll();
|
||||
});
|
||||
|
||||
addTask('app/update folders and tags', async () => {
|
||||
await refreshFolders((action) => this.dispatch(action), '');
|
||||
|
||||
const tags = await Tag.allWithNotes();
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
items: tags,
|
||||
});
|
||||
});
|
||||
|
||||
addTask('app/set up custom CSS', async () => {
|
||||
await this.setupCustomCss();
|
||||
});
|
||||
await this.setupCustomCss();
|
||||
|
||||
// const masterKeys = await MasterKey.all();
|
||||
|
||||
@@ -529,237 +514,188 @@ class Application extends BaseApplication {
|
||||
// items: masterKeys,
|
||||
// });
|
||||
|
||||
addTask('app/send initial selection to redux', async () => {
|
||||
const getNotesParent = async () => {
|
||||
let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
|
||||
if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
|
||||
notesParent = {
|
||||
type: 'Folder',
|
||||
selectedItemId: Setting.value('activeFolderId'),
|
||||
};
|
||||
}
|
||||
return notesParent;
|
||||
};
|
||||
|
||||
const notesParent = await getNotesParent();
|
||||
if (notesParent.type === 'SmartFilter') {
|
||||
this.store().dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else if (notesParent.type === 'Tag') {
|
||||
this.store().dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
const getNotesParent = async () => {
|
||||
let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
|
||||
if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
|
||||
notesParent = {
|
||||
type: 'Folder',
|
||||
selectedItemId: Setting.value('activeFolderId'),
|
||||
};
|
||||
}
|
||||
return notesParent;
|
||||
};
|
||||
|
||||
const notesParent = await getNotesParent();
|
||||
|
||||
if (notesParent.type === 'SmartFilter') {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
|
||||
} else if (notesParent.type === 'Tag') {
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
type: 'TAG_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
addTask('app/initializeUserFetcher', async () => {
|
||||
initializeUserFetcher();
|
||||
shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
});
|
||||
|
||||
addTask('app/updateTray', () => this.updateTray());
|
||||
// Always disable on Mac for now - and disable too for the few apps that may have the flag enabled.
|
||||
// At present, it only seems to work on Windows.
|
||||
if (shim.isMac()) {
|
||||
Setting.setValue('featureFlag.autoUpdaterServiceEnabled', false);
|
||||
}
|
||||
|
||||
addTask('app/set main window state', () => {
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().mainWindow().hide();
|
||||
} else {
|
||||
bridge().mainWindow().show();
|
||||
// Note: Auto-update is a misnomer in the code.
|
||||
// The code below only checks, if a new version is available.
|
||||
// We only allow Windows and macOS users to automatically check for updates
|
||||
if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
const runAutoUpdateCheck = () => {
|
||||
if (Setting.value('autoUpdateEnabled')) {
|
||||
void checkForUpdates(true, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check on startup
|
||||
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
|
||||
// Then every x hours
|
||||
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addTask('app/start maintenance tasks', () => {
|
||||
// Always disable on Mac for now - and disable too for the few apps that may have the flag enabled.
|
||||
// At present, it only seems to work on Windows.
|
||||
if (shim.isMac()) {
|
||||
Setting.setValue('featureFlag.autoUpdaterServiceEnabled', false);
|
||||
}
|
||||
initializeUserFetcher();
|
||||
shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
|
||||
// Note: Auto-update is a misnomer in the code.
|
||||
// The code below only checks, if a new version is available.
|
||||
// We only allow Windows and macOS users to automatically check for updates
|
||||
if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
const runAutoUpdateCheck = () => {
|
||||
if (Setting.value('autoUpdateEnabled')) {
|
||||
void checkForUpdates(true, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
}
|
||||
};
|
||||
this.updateTray();
|
||||
|
||||
// Initial check on startup
|
||||
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
|
||||
// Then every x hours
|
||||
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
shim.setTimeout(() => {
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
shim.setTimeout(() => {
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
void ShareService.instance().maintenance();
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().mainWindow().hide();
|
||||
} else {
|
||||
bridge().mainWindow().show();
|
||||
}
|
||||
|
||||
ResourceService.runInBackground();
|
||||
void ShareService.instance().maintenance();
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
ResourceService.runInBackground();
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
void AlarmService.updateAllNotifications();
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
void AlarmService.updateAllNotifications();
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
void AlarmService.updateAllNotifications();
|
||||
|
||||
void DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
}
|
||||
|
||||
RevisionService.instance().runInBackground();
|
||||
this.startRotatingLogMaintenance(Setting.value('profileDir'));
|
||||
});
|
||||
|
||||
addTask('app/set up ClipperServer', () => {
|
||||
const clipperLogger = new Logger();
|
||||
clipperLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-clipper.txt` });
|
||||
clipperLogger.addTarget(TargetType.Console);
|
||||
|
||||
ClipperServer.instance().initialize(actionApi);
|
||||
ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
ClipperServer.instance().setDispatch(this.store().dispatch);
|
||||
|
||||
if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
});
|
||||
|
||||
addTask('app/set up external edit watchers', () => {
|
||||
ExternalEditWatcher.instance().setLogger(reg.logger());
|
||||
ExternalEditWatcher.instance().initialize(bridge, this.store().dispatch);
|
||||
|
||||
ResourceEditWatcher.instance().initialize(
|
||||
reg.logger(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(action: any) => { this.store().dispatch(action); },
|
||||
(path: string) => bridge().openItem(path),
|
||||
() => this.store().getState().windowId,
|
||||
);
|
||||
|
||||
// Forwards the local event to the global event manager, so that it can
|
||||
// be picked up by the plugin manager.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
ResourceEditWatcher.instance().on('resourceChange', (event: any) => {
|
||||
eventManager.emit(EventName.ResourceChange, event);
|
||||
void DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
}
|
||||
|
||||
const clipperLogger = new Logger();
|
||||
clipperLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-clipper.txt` });
|
||||
clipperLogger.addTarget(TargetType.Console);
|
||||
|
||||
ClipperServer.instance().initialize(actionApi);
|
||||
ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
ClipperServer.instance().setDispatch(this.store().dispatch);
|
||||
|
||||
if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
|
||||
ExternalEditWatcher.instance().setLogger(reg.logger());
|
||||
ExternalEditWatcher.instance().initialize(bridge, this.store().dispatch);
|
||||
|
||||
ResourceEditWatcher.instance().initialize(
|
||||
reg.logger(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(action: any) => { this.store().dispatch(action); },
|
||||
(path: string) => bridge().openItem(path),
|
||||
() => this.store().getState().windowId,
|
||||
);
|
||||
|
||||
// Forwards the local event to the global event manager, so that it can
|
||||
// be picked up by the plugin manager.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
ResourceEditWatcher.instance().on('resourceChange', (event: any) => {
|
||||
eventManager.emit(EventName.ResourceChange, event);
|
||||
});
|
||||
|
||||
RevisionService.instance().runInBackground();
|
||||
|
||||
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
||||
if (Setting.value('env') === 'dev') {
|
||||
addTask('app/add debug variables', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(window as any).joplin = {
|
||||
revisionService: RevisionService.instance(),
|
||||
migrationService: MigrationService.instance(),
|
||||
decryptionWorker: DecryptionWorker.instance(),
|
||||
commandService: CommandService.instance(),
|
||||
pluginService: PluginService.instance(),
|
||||
bridge: bridge(),
|
||||
debug: new DebugService(reg.db()),
|
||||
resourceService: ResourceService.instance(),
|
||||
searchEngine: SearchEngine.instance(),
|
||||
ocrService: () => this.ocrService_,
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(window as any).joplin = {
|
||||
revisionService: RevisionService.instance(),
|
||||
migrationService: MigrationService.instance(),
|
||||
decryptionWorker: DecryptionWorker.instance(),
|
||||
commandService: CommandService.instance(),
|
||||
pluginService: PluginService.instance(),
|
||||
bridge: bridge(),
|
||||
debug: new DebugService(reg.db()),
|
||||
resourceService: ResourceService.instance(),
|
||||
searchEngine: SearchEngine.instance(),
|
||||
ocrService: () => this.ocrService_,
|
||||
};
|
||||
}
|
||||
|
||||
addTask('app/listen for main process events', () => {
|
||||
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
|
||||
bridge().setOnAllowedExtensionsChangeListener((newExtensions) => {
|
||||
Setting.setValue('linking.extraAllowedExtensions', newExtensions);
|
||||
});
|
||||
|
||||
ipcRenderer.on('window-focused', (_event, newWindowId) => {
|
||||
const currentWindowId = this.store().getState().windowId;
|
||||
if (newWindowId !== currentWindowId) {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: newWindowId,
|
||||
lastWindowId: currentWindowId,
|
||||
});
|
||||
}
|
||||
});
|
||||
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
|
||||
bridge().setOnAllowedExtensionsChangeListener((newExtensions) => {
|
||||
Setting.setValue('linking.extraAllowedExtensions', newExtensions);
|
||||
});
|
||||
|
||||
addTask('app/initPluginService', () => this.initPluginService());
|
||||
|
||||
addTask('app/setupContextMenu', () => {
|
||||
this.setupContextMenu();
|
||||
ipcRenderer.on('window-focused', (_event, newWindowId) => {
|
||||
const currentWindowId = this.store().getState().windowId;
|
||||
if (newWindowId !== currentWindowId) {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: newWindowId,
|
||||
lastWindowId: currentWindowId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTask('app/set up SpellCheckerService', async () => {
|
||||
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
|
||||
await this.initPluginService();
|
||||
|
||||
this.setupContextMenu();
|
||||
|
||||
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
|
||||
|
||||
this.startRotatingLogMaintenance(Setting.value('profileDir'));
|
||||
|
||||
await this.setupOcrService();
|
||||
|
||||
eventManager.on(EventName.OcrServiceResourcesProcessed, async () => {
|
||||
await ResourceService.instance().indexNoteResources();
|
||||
});
|
||||
|
||||
addTask('app/listen for resource events', () => {
|
||||
eventManager.on(EventName.OcrServiceResourcesProcessed, async () => {
|
||||
await ResourceService.instance().indexNoteResources();
|
||||
});
|
||||
|
||||
eventManager.on(EventName.NoteResourceIndexed, async () => {
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
});
|
||||
eventManager.on(EventName.NoteResourceIndexed, async () => {
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
});
|
||||
|
||||
addTask('app/setupOcrService', () => this.setupOcrService());
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async start(argv: string[], startOptions: StartOptions = null): Promise<any> {
|
||||
const startupTask = perfLogger.taskStart('app/start');
|
||||
|
||||
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
|
||||
// insert an extra argument so that they can be processed in a consistent way everywhere.
|
||||
if (!bridge().electronIsDev()) argv.splice(1, 0, '.');
|
||||
|
||||
|
||||
argv = await super.start(argv, startOptions);
|
||||
|
||||
await this.setupIntegrationTestUtils();
|
||||
|
||||
bridge().setLogFilePath(Logger.globalLogger.logFilePath());
|
||||
await this.applySettingsSideEffects();
|
||||
|
||||
if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) {
|
||||
reg.logger().info('app.start: doing upgradeSyncTarget action');
|
||||
bridge().mainWindow().show();
|
||||
startupTask.onEnd();
|
||||
|
||||
return { action: 'upgradeSyncTarget' };
|
||||
}
|
||||
|
||||
const startupTasks = this.buildStartupTasks_();
|
||||
for (const task of startupTasks) {
|
||||
await perfLogger.track(task.label, async () => task.task());
|
||||
}
|
||||
|
||||
// Used by tests
|
||||
ipcRenderer.send('startup-finished');
|
||||
|
||||
// setTimeout(() => {
|
||||
// void populateDatabase(reg.db(), {
|
||||
@@ -813,10 +749,6 @@ class Application extends BaseApplication {
|
||||
|
||||
// await runIntegrationTests();
|
||||
|
||||
// Used by tests
|
||||
ipcRenderer.send('startup-finished');
|
||||
|
||||
startupTask.onEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { urlDecode } from '@joplin/lib/string-utils';
|
||||
import * as Sentry from '@sentry/electron/main';
|
||||
import { homedir } from 'os';
|
||||
import { msleep } from '@joplin/utils/time';
|
||||
import { pathExists, pathExistsSync, writeFileSync, ensureDirSync } from 'fs-extra';
|
||||
import { extname, normalize, join } from 'path';
|
||||
import { pathExists, pathExistsSync, writeFileSync } from 'fs-extra';
|
||||
import { extname, normalize } from 'path';
|
||||
import isSafeToOpen from './utils/isSafeToOpen';
|
||||
import { closeSync, openSync, readSync, statSync } from 'fs';
|
||||
import { KB } from '@joplin/utils/bytes';
|
||||
@@ -67,30 +67,6 @@ export class Bridge {
|
||||
this.logFilePath_ = v;
|
||||
}
|
||||
|
||||
private getCrashDumpDirectory(): string {
|
||||
try {
|
||||
const platformName = shim.platformName();
|
||||
switch (platformName) {
|
||||
case 'win32':
|
||||
// Windows: Use %LOCALAPPDATA%\CrashDumps
|
||||
return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'CrashDumps');
|
||||
case 'darwin':
|
||||
// macOS: Use ~/Library/Logs/DiagnosticReports
|
||||
return join(homedir(), 'Library', 'Logs', 'DiagnosticReports');
|
||||
case 'linux':
|
||||
// Linux: Use XDG_STATE_HOME (for logs) or fallback to ~/.local/state
|
||||
return join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'joplin');
|
||||
default:
|
||||
// For unknown platforms, default to the home directory
|
||||
return homedir();
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't get the platform name, fallback to the home directory
|
||||
return homedir();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private sentryInit() {
|
||||
const getLogLines = () => {
|
||||
try {
|
||||
@@ -133,10 +109,7 @@ export class Bridge {
|
||||
log: logAttachment ? logAttachment.data.trim().split('\n') : [],
|
||||
};
|
||||
|
||||
const crashDumpDir = this.getCrashDumpDirectory();
|
||||
ensureDirSync(crashDumpDir);
|
||||
const crashDumpPath = join(crashDumpDir, `joplin_crash_dump_${date}.json`);
|
||||
writeFileSync(crashDumpPath, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8');
|
||||
writeFileSync(`${homedir()}/joplin_crash_dump_${date}.json`, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8');
|
||||
} catch (error) {
|
||||
// Ignore the error since we can't handle it here
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import * as convertHtmlToMarkdown from './convertNoteToMarkdown';
|
||||
import { AppState, createAppDefaultState } from '../app.reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
describe('convertNoteToMarkdown', () => {
|
||||
let state: AppState = undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
state = createAppDefaultState({});
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should set the original note to be trashed', async () => {
|
||||
const folder = await Folder.save({ title: 'test_folder' });
|
||||
const htmlNote = await Note.save({ title: 'test', body: '<p>Hello</p>', parent_id: folder.id, markup_language: MarkupLanguage.Html });
|
||||
state.selectedNoteIds = [htmlNote.id];
|
||||
|
||||
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: () => {} });
|
||||
|
||||
const refreshedNote = await Note.load(htmlNote.id);
|
||||
|
||||
expect(htmlNote.deleted_time).toBe(0);
|
||||
expect(refreshedNote.deleted_time).not.toBe(0);
|
||||
});
|
||||
|
||||
it('should recreate a new note that is a clone of the original', async () => {
|
||||
let noteConvertedToMarkdownId = '';
|
||||
const dispatchFn = jest.fn()
|
||||
.mockImplementationOnce(() => {})
|
||||
.mockImplementationOnce(action => {
|
||||
noteConvertedToMarkdownId = action.id;
|
||||
});
|
||||
|
||||
const folder = await Folder.save({ title: 'test_folder' });
|
||||
const htmlNoteProperties = {
|
||||
title: 'test',
|
||||
body: '<p>Hello</p>',
|
||||
parent_id: folder.id,
|
||||
markup_language: MarkupLanguage.Html,
|
||||
author: 'test-author',
|
||||
is_todo: 1,
|
||||
todo_completed: 1,
|
||||
};
|
||||
const htmlNote = await Note.save(htmlNoteProperties);
|
||||
state.selectedNoteIds = [htmlNote.id];
|
||||
|
||||
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: dispatchFn });
|
||||
|
||||
expect(dispatchFn).toHaveBeenCalledTimes(2);
|
||||
expect(noteConvertedToMarkdownId).not.toBe('');
|
||||
|
||||
const markdownNote = await Note.load(noteConvertedToMarkdownId);
|
||||
|
||||
const fields: (keyof NoteEntity)[] = ['parent_id', 'title', 'author', 'is_todo', 'todo_completed'];
|
||||
for (const field of fields) {
|
||||
expect(htmlNote[field]).toEqual(markdownNote[field]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should generate action to trigger notification', async () => {
|
||||
let originalHtmlNoteId = '';
|
||||
let actionType = '';
|
||||
const dispatchFn = jest.fn()
|
||||
.mockImplementationOnce(action => {
|
||||
originalHtmlNoteId = action.value;
|
||||
actionType = action.type;
|
||||
})
|
||||
.mockImplementationOnce(() => {});
|
||||
|
||||
const folder = await Folder.save({ title: 'test_folder' });
|
||||
const htmlNoteProperties = {
|
||||
title: 'test',
|
||||
body: '<p>Hello</p>',
|
||||
parent_id: folder.id,
|
||||
markup_language: MarkupLanguage.Html,
|
||||
author: 'test-author',
|
||||
is_todo: 1,
|
||||
todo_completed: 1,
|
||||
};
|
||||
const htmlNote = await Note.save(htmlNoteProperties);
|
||||
state.selectedNoteIds = [htmlNote.id];
|
||||
|
||||
await convertHtmlToMarkdown.runtime().execute({ state, dispatch: dispatchFn });
|
||||
|
||||
expect(dispatchFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(originalHtmlNoteId).toBe(htmlNote.id);
|
||||
expect(actionType).toBe('NOTE_HTML_TO_MARKDOWN_DONE');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { runtime as convertHtmlToMarkdown } from '@joplin/lib/commands/convertHtmlToMarkdown';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'convertNoteToMarkdown',
|
||||
label: () => _('Convert note to Markdown'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteId: string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
const note = await Note.load(noteId);
|
||||
|
||||
if (!note) return;
|
||||
|
||||
try {
|
||||
const markdownBody = await convertHtmlToMarkdown().execute(context, note.body);
|
||||
|
||||
const newNote = await Note.duplicate(note.id);
|
||||
|
||||
newNote.body = markdownBody;
|
||||
newNote.markup_language = MarkupLanguage.Markdown;
|
||||
|
||||
await Note.save(newNote);
|
||||
|
||||
await Note.delete(note.id, { toTrash: true });
|
||||
|
||||
context.dispatch({
|
||||
type: 'NOTE_HTML_TO_MARKDOWN_DONE',
|
||||
value: note.id,
|
||||
});
|
||||
|
||||
context.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: newNote.id,
|
||||
});
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(_('Could not convert note to Markdown: %s', error.message));
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected && noteIsHtml && !noteIsReadOnly',
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as convertNoteToMarkdown from './convertNoteToMarkdown';
|
||||
import * as copyDevCommand from './copyDevCommand';
|
||||
import * as copyToClipboard from './copyToClipboard';
|
||||
import * as editProfileConfig from './editProfileConfig';
|
||||
@@ -25,7 +24,6 @@ import * as toggleSafeMode from './toggleSafeMode';
|
||||
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
||||
|
||||
const index: any[] = [
|
||||
convertNoteToMarkdown,
|
||||
copyDevCommand,
|
||||
copyToClipboard,
|
||||
editProfileConfig,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Dispatch } from 'redux';
|
||||
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
|
||||
import { NotificationType } from '../PopupNotification/types';
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const popupManager = useContext(PopupNotificationContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.noteId || props.noteId === '') return;
|
||||
|
||||
props.dispatch({ type: 'NOTE_HTML_TO_MARKDOWN_DONE', value: '' });
|
||||
|
||||
const notification = popupManager.createPopup(() => (
|
||||
<div>{_('The note has been converted to Markdown and the original note has been moved to the trash')}</div>
|
||||
), { type: NotificationType.Success });
|
||||
notification.scheduleDismiss();
|
||||
}, [props.dispatch, popupManager, props.noteId]);
|
||||
|
||||
return <div style={{ display: 'none' }}/>;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
width: number;
|
||||
@@ -9,62 +9,40 @@ interface Props {
|
||||
const fontSizeCache_: Record<string, number> = {};
|
||||
|
||||
export default (props: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef(null);
|
||||
const [containerReady, setContainerReady] = useState(false);
|
||||
|
||||
const refCallback = useCallback((el: HTMLDivElement | null) => {
|
||||
if (el && !containerRef.current) {
|
||||
containerRef.current = el;
|
||||
requestAnimationFrame(() => {
|
||||
setContainerReady(true);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fontSize = useMemo(() => {
|
||||
if (!containerReady || !containerRef.current) {
|
||||
return Math.min(props.height * 0.7, 14);
|
||||
}
|
||||
if (!containerReady) return props.height;
|
||||
|
||||
const cacheKey = [props.width, props.height, props.emoji].join('-');
|
||||
if (fontSizeCache_[cacheKey]) {
|
||||
return fontSizeCache_[cacheKey];
|
||||
}
|
||||
|
||||
// Set the emoji font size so that it fits within the specified width
|
||||
// and height. In fact, currently it only looks at the height.
|
||||
|
||||
let spanFontSize = props.height;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.innerText = props.emoji;
|
||||
span.style.fontSize = `${spanFontSize}px`;
|
||||
span.style.visibility = 'hidden';
|
||||
span.style.position = 'absolute';
|
||||
span.style.whiteSpace = 'nowrap';
|
||||
containerRef.current.appendChild(span);
|
||||
|
||||
let rect = span.getBoundingClientRect();
|
||||
while ((rect.height > props.height || rect.width > props.width) && spanFontSize > 1) {
|
||||
spanFontSize -= 0.5;
|
||||
|
||||
while (rect.height > props.height) {
|
||||
spanFontSize -= .5;
|
||||
span.style.fontSize = `${spanFontSize}px`;
|
||||
rect = span.getBoundingClientRect();
|
||||
}
|
||||
|
||||
span.remove();
|
||||
|
||||
fontSizeCache_[cacheKey] = spanFontSize;
|
||||
return spanFontSize;
|
||||
}, [props.width, props.height, props.emoji, containerReady]);
|
||||
}, [props.width, props.height, props.emoji, containerReady, containerRef]);
|
||||
|
||||
return <div
|
||||
ref={refCallback}
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
fontSize,
|
||||
}}
|
||||
>
|
||||
{props.emoji}
|
||||
</div>;
|
||||
return <div className="emoji-box" ref={el => { containerRef.current = el; setContainerReady(true); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: props.width, height: props.height, fontSize }}>{props.emoji}</div>;
|
||||
};
|
||||
|
||||
@@ -38,14 +38,12 @@ import restart from '../services/restart';
|
||||
import { connect } from 'react-redux';
|
||||
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import validateColumns from './NoteListHeader/utils/validateColumns';
|
||||
import ConversionNotification from './ConversionNotification/ConversionNotification';
|
||||
import TrashNotification from './TrashNotification/TrashNotification';
|
||||
import UpdateNotification from './UpdateNotification/UpdateNotification';
|
||||
import NoteEditor from './NoteEditor/NoteEditor';
|
||||
import PluginNotification from './PluginNotification/PluginNotification';
|
||||
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
@@ -86,7 +84,6 @@ interface Props {
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
toast: Toast;
|
||||
shouldSwitchToAppleSiliconVersion: boolean;
|
||||
noteHtmlToMarkdownDone: string;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -800,10 +797,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<ConversionNotification
|
||||
noteId={this.props.noteHtmlToMarkdownDone}
|
||||
dispatch={this.props.dispatch as Dispatch}
|
||||
/>
|
||||
<TrashNotification
|
||||
lastDeletion={this.props.lastDeletion}
|
||||
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
|
||||
@@ -860,7 +853,6 @@ const mapStateToProps = (state: AppState) => {
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
toast: state.toast,
|
||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
|
||||
noteHtmlToMarkdownDone: state.noteHtmlToMarkdownDone,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -803,7 +803,6 @@ function useMenu(props: Props) {
|
||||
menuItemDic.toggleNoteList,
|
||||
menuItemDic.toggleVisiblePanes,
|
||||
menuItemDic.toggleEditorPlugin,
|
||||
menuItemDic.toggleEditors,
|
||||
{
|
||||
label: _('Layout button sequence'),
|
||||
submenu: layoutButtonSequenceMenuItems,
|
||||
@@ -907,7 +906,6 @@ function useMenu(props: Props) {
|
||||
separator(),
|
||||
menuItemDic.setTags,
|
||||
menuItemDic.showShareNoteDialog,
|
||||
menuItemDic.convertNoteToMarkdown,
|
||||
separator(),
|
||||
menuItemDic.showNoteProperties,
|
||||
menuItemDic.showNoteContentProperties,
|
||||
|
||||
@@ -340,8 +340,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
props.setShowLocalSearch(event.searchState.dialogVisible);
|
||||
}
|
||||
lastSearchState.current = event.searchState;
|
||||
} else if (event.kind === EditorEventType.FollowLink) {
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||
|
||||
@@ -364,8 +362,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
readOnly: props.disabled,
|
||||
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
inlineRenderingEnabled: Setting.value('editor.inlineRendering'),
|
||||
imageRenderingEnabled: Setting.value('editor.imageRendering'),
|
||||
themeData: {
|
||||
...styles.globalTheme,
|
||||
marginLeft: 0,
|
||||
@@ -414,7 +410,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
onSelectPastBeginning={onSelectPastBeginning}
|
||||
externalSearch={props.searchMarkers}
|
||||
useLocalSearch={props.useLocalSearch}
|
||||
onLocalize={_}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,10 +15,6 @@ import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||
import localisation from './utils/localisation';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { parseResourceUrl } from '@joplin/lib/urlUtils';
|
||||
import { resourceFilename } from '@joplin/lib/models/utils/resourceUtils';
|
||||
import getResourceBaseUrl from '../../../utils/getResourceBaseUrl';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
@@ -108,15 +104,7 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
onLogMessage: message => onLogMessageRef.current(message),
|
||||
};
|
||||
|
||||
const editor = createEditor(editorContainerRef.current, {
|
||||
...editorProps,
|
||||
resolveImageSrc: async src => {
|
||||
const url = parseResourceUrl(src);
|
||||
if (!url.itemId) return null;
|
||||
const item = await Resource.load(url.itemId);
|
||||
return `${getResourceBaseUrl()}/${resourceFilename(item)}`;
|
||||
},
|
||||
});
|
||||
const editor = createEditor(editorContainerRef.current, editorProps);
|
||||
editor.addStyles({
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
'&.CodeMirror': {
|
||||
|
||||
@@ -18,12 +18,11 @@ const logger = Logger.create('shouldPasteResources');
|
||||
// instead the clipboard resources, which will contain the actual image.
|
||||
//
|
||||
// We have a lot of log statements so that if someone reports a bug we can ask
|
||||
// them to check the console and give us the messages they have. However, to avoid
|
||||
// including sensitive information in the logs, users will need to check the console,
|
||||
// not the log file.
|
||||
// them to check the console and give us the messages they have.
|
||||
export default (pastedText: string, pastedHtml: string, resourceMds: string[]) => {
|
||||
const debugInformation = JSON.stringify({ pastedText, pastedHtml, resourceMds }, undefined, '\t');
|
||||
logger.debug('Input data:', debugInformation);
|
||||
logger.info('Pasted text:', pastedText);
|
||||
logger.info('Pasted HTML:', pastedHtml);
|
||||
logger.info('Resources:', resourceMds);
|
||||
|
||||
if (pastedText) {
|
||||
logger.info('Not pasting resources only because the clipboard contains plain text');
|
||||
|
||||
@@ -5,7 +5,6 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
const useLinkTooltips = (editor: Editor|null) => {
|
||||
const resetModifiedTitles = useCallback(() => {
|
||||
if (!editor) return;
|
||||
for (const element of editor.getDoc().querySelectorAll('a[data-joplin-original-title]')) {
|
||||
element.setAttribute('title', element.getAttribute('data-joplin-original-title') ?? '');
|
||||
element.removeAttribute('data-joplin-original-title');
|
||||
|
||||
@@ -56,7 +56,6 @@ import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@@ -170,7 +169,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml(props.plugins, {
|
||||
resourceBaseUrl: getResourceBaseUrl(),
|
||||
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
@@ -467,7 +466,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
// It is currently used to remember pdf scroll position for each attachments of each note uniquely.
|
||||
noteId: props.noteId,
|
||||
watchedNoteFiles: props.watchedNoteFiles,
|
||||
enableHtmlToMarkdownBanner: props.enableHtmlToMarkdownBanner,
|
||||
};
|
||||
|
||||
let editor = null;
|
||||
@@ -490,17 +488,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
setShowRevisions(false);
|
||||
}, []);
|
||||
|
||||
const onBannerConvertItToMarkdown = useCallback(async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
if (!props.selectedNoteIds || props.selectedNoteIds.length === 0) return;
|
||||
await CommandService.instance().execute('convertNoteToMarkdown', props.selectedNoteIds[0]);
|
||||
}, [props.selectedNoteIds]);
|
||||
|
||||
const onHideBannerConvertItToMarkdown = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
Setting.setValue('editor.enableHtmlToMarkdownBanner', false);
|
||||
};
|
||||
|
||||
const onBannerResourceClick = useCallback(async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
const resourceId = event.currentTarget.getAttribute('data-resource-id');
|
||||
@@ -645,30 +632,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
function renderConvertHtmlToMarkdown(): React.ReactNode {
|
||||
if (!props.enableHtmlToMarkdownBanner) return null;
|
||||
|
||||
const note = props.notes.find(n => n.id === props.selectedNoteIds[0]);
|
||||
if (!note) return null;
|
||||
if (note.markup_language !== MarkupLanguage.Html) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.resourceWatchBanner}>
|
||||
<p style={styles.resourceWatchBannerLine}>
|
||||
{_('This note is in HTML format. Convert it to Markdown to edit it more easily.')}
|
||||
|
||||
<a href="#" style={styles.resourceWatchBannerAction} onClick={onBannerConvertItToMarkdown}>{`${_('Convert it')}`}</a>
|
||||
{' / '}
|
||||
<a href="#" style={styles.resourceWatchBannerAction} onClick={onHideBannerConvertItToMarkdown}>{_('Don\'t show this message again')}</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.root} onDragOver={onDragOver} onDrop={onDrop} ref={containerRef}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{renderConvertHtmlToMarkdown()}
|
||||
{renderResourceWatchingNotification()}
|
||||
{renderResourceInSearchResultsNotification()}
|
||||
<NoteTitleBar
|
||||
@@ -756,7 +722,6 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
syncUserId: state.settings['sync.userId'],
|
||||
shareCacheSetting: state.settings['sync.shareCache'],
|
||||
searchResults: state.searchResults,
|
||||
enableHtmlToMarkdownBanner: state.settings['editor.enableHtmlToMarkdownBanner'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import { AppState } from '../../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import BannerContent from './BannerContent';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import onRichTextReadMoreLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick';
|
||||
import onRichTextDismissLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
@@ -17,6 +16,14 @@ interface Props {
|
||||
plugins: PluginStates;
|
||||
}
|
||||
|
||||
const onRichTextDismissLinkClick = () => {
|
||||
Setting.setValue('richTextBannerDismissed', true);
|
||||
};
|
||||
|
||||
const onRichTextReadMoreLinkClick = () => {
|
||||
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
|
||||
};
|
||||
|
||||
const onSwitchToLegacyEditor = () => {
|
||||
Setting.setValue('editor.legacyMarkdown', true);
|
||||
};
|
||||
|
||||
@@ -69,10 +69,6 @@ export default function styles(props: NoteEditorProps) {
|
||||
marginTop: 0,
|
||||
marginBottom: 10,
|
||||
},
|
||||
resourceWatchBannerAction: {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colorWarnUrl,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@ const MenuItem = bridge().MenuItem;
|
||||
import Resource, { resourceOcrStatusToString } from '@joplin/lib/models/Resource';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { NoteEntity, ResourceEntity, ResourceOcrDriverId, ResourceOcrStatus } from '@joplin/lib/services/database/types';
|
||||
import { NoteEntity, ResourceEntity, ResourceOcrStatus } from '@joplin/lib/services/database/types';
|
||||
import { TinyMceEditorEvents } from '../NoteBody/TinyMCE/utils/types';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import shim, { MessageBoxType } from '@joplin/lib/shim';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { openFileWithExternalEditor } from '@joplin/lib/services/ExternalEditWatcher/utils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const fs = require('fs-extra');
|
||||
const { writeFile } = require('fs-extra');
|
||||
const { clipboard } = require('electron');
|
||||
@@ -138,40 +137,6 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||
},
|
||||
recognizeHandwrittenImage: {
|
||||
label: _('Recognize handwritten image'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
if (!SyncTargetRegistry.isJoplinServerOrCloud(syncTargetId)) {
|
||||
await shim.showMessageBox(_('This feature is only available on Joplin Cloud and Joplin Server.'), { type: MessageBoxType.Error });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Setting.value('ocr.handwrittenTextDriverEnabled')) {
|
||||
await shim.showMessageBox(_('This feature is disabled by default, you need to manually enable it by turning on the option to \'Enable handwritten transcription\'.'), { type: MessageBoxType.Error });
|
||||
return;
|
||||
}
|
||||
|
||||
const { resource } = await resourceInfo(options);
|
||||
|
||||
if (!['image/png', 'image/jpg', 'image/jpeg', 'image/bmp'].includes(resource.mime)) {
|
||||
await shim.showMessageBox(_('This image type is not supported by the recognition system.'), { type: MessageBoxType.Error });
|
||||
return;
|
||||
}
|
||||
|
||||
await Resource.save({
|
||||
id: resource.id,
|
||||
ocr_status: ResourceOcrStatus.Todo,
|
||||
ocr_driver_id: ResourceOcrDriverId.HandwrittenText,
|
||||
ocr_details: '',
|
||||
ocr_error: '',
|
||||
ocr_text: '',
|
||||
});
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => {
|
||||
return itemType === ContextMenuItemType.Resource || (itemType === ContextMenuItemType.Image && options.resourceId);
|
||||
},
|
||||
},
|
||||
revealInFolder: {
|
||||
label: _('Reveal file in folder'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const getResourceBaseUrl = () => `joplin-content://note-viewer/${Setting.value('resourceDir')}/`;
|
||||
export default getResourceBaseUrl;
|
||||
@@ -67,7 +67,6 @@ export interface NoteEditorProps {
|
||||
onTitleChange?: (title: string)=> void;
|
||||
bodyEditor: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
enableHtmlToMarkdownBanner: boolean;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorRef {
|
||||
@@ -139,7 +138,6 @@ export interface NoteBodyEditorProps {
|
||||
noteId: string;
|
||||
useCustomPdfViewer: boolean;
|
||||
watchedNoteFiles: string[];
|
||||
enableHtmlToMarkdownBanner: boolean;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorPropsAndRef extends NoteBodyEditorProps {
|
||||
|
||||
@@ -49,7 +49,7 @@ const useScheduleSaveCallbacks = (props: Props) => {
|
||||
}, [props.dispatch, props.editorId, props.setFormNote]);
|
||||
|
||||
const saveNoteIfWillChange = useCallback(async (formNote: FormNote) => {
|
||||
if (!formNote.id || !formNote.bodyWillChangeId || !props.editorRef.current) return;
|
||||
if (!formNote.id || !formNote.bodyWillChangeId) return;
|
||||
|
||||
const body = await props.editorRef.current.content();
|
||||
|
||||
|
||||
@@ -411,14 +411,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
const ll = this.latLongFromLocation(value);
|
||||
url = Note.geoLocationUrlFromLatLong(ll.latitude, ll.longitude);
|
||||
}
|
||||
const urlStyle: React.CSSProperties = {
|
||||
...theme.urlStyle,
|
||||
maxWidth: '180px',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
};
|
||||
const urlStyle: React.CSSProperties = { ...theme.urlStyle, maxWidth: '180px', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' };
|
||||
controlComp = (
|
||||
<a href="#" onClick={() => bridge().openExternal(url)} style={urlStyle}>
|
||||
{displayedValue}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from './Dialog';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { formatDateTimeLocalToMs, isValidDate } from '@joplin/utils/time';
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -118,15 +117,6 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
// The button to change the date/time cannot be customized easily so we need to use the
|
||||
// light theme for that particular component.
|
||||
this.styles_.dateTimeInput = {
|
||||
...this.styles_.input,
|
||||
color: lightTheme.color,
|
||||
backgroundColor: lightTheme.backgroundColor,
|
||||
borderColor: lightTheme.dividerColor,
|
||||
};
|
||||
|
||||
this.styles_.select = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
control: (provided: any) => {
|
||||
@@ -266,7 +256,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
onChange={onChange}
|
||||
type="datetime-local"
|
||||
className='datetime-picker'
|
||||
style={styles.dateTimeInput}
|
||||
style={styles.input}
|
||||
/>;
|
||||
} else if (this.props.inputType === 'tags') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -27,7 +27,7 @@ const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
|
||||
const icon = props.allFoldersCollapsed ? 'far fa-caret-square-right' : 'far fa-caret-square-down';
|
||||
const label = props.allFoldersCollapsed ? _('Expand all notebooks') : _('Collapse all notebooks');
|
||||
|
||||
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall' title={label}>
|
||||
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall'>
|
||||
<i
|
||||
aria-label={label}
|
||||
role='img'
|
||||
@@ -39,11 +39,9 @@ const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
|
||||
const NewFolderButton = () => {
|
||||
// To allow it to be accessed by accessibility tools, the new folder button
|
||||
// is not included in the portion of the list with role='tree'.
|
||||
const label = _('New notebook');
|
||||
|
||||
return <button onClick={onAddFolderButtonClick} className='sidebar-header-button -newfolder' title={label}>
|
||||
return <button onClick={onAddFolderButtonClick} className='sidebar-header-button -newfolder'>
|
||||
<i
|
||||
aria-label={label}
|
||||
aria-label={_('New notebook')}
|
||||
role='img'
|
||||
className='fas fa-plus'
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,5 @@ export default function() {
|
||||
'switchProfile3',
|
||||
'pasteAsText',
|
||||
'showNoteProperties',
|
||||
'convertNoteToMarkdown',
|
||||
'toggleEditors',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,36 +2,35 @@
|
||||
|
||||
// Disable React message in console "Download the React DevTools for a better development experience"
|
||||
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
|
||||
// eslint-disable-next-line no-undef, @typescript-eslint/no-explicit-any
|
||||
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
||||
// eslint-disable-next-line no-undef
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
||||
supportsFiber: true,
|
||||
inject: function() {},
|
||||
onCommitFiberRoot: function() {},
|
||||
onCommitFiberUnmount: function() {},
|
||||
};
|
||||
|
||||
import './utils/sourceMapSetup';
|
||||
import app from './app';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import NoteTag from '@joplin/lib/models/NoteTag';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import Revision from '@joplin/lib/models/Revision';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import bridge from './services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
require('./utils/sourceMapSetup');
|
||||
const app = require('./app').default;
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const Resource = require('@joplin/lib/models/Resource').default;
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem').default;
|
||||
const Note = require('@joplin/lib/models/Note').default;
|
||||
const Tag = require('@joplin/lib/models/Tag').default;
|
||||
const NoteTag = require('@joplin/lib/models/NoteTag').default;
|
||||
const MasterKey = require('@joplin/lib/models/MasterKey').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const Revision = require('@joplin/lib/models/Revision').default;
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
|
||||
const bridge = require('./services/bridge').default;
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import FileApiDriverLocal from '@joplin/lib/file-api-driver-local';
|
||||
import * as React from 'react';
|
||||
import nodeSqlite = require('sqlite3');
|
||||
import initLib from '@joplin/lib/initLib';
|
||||
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
||||
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
|
||||
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
|
||||
const React = require('react');
|
||||
const nodeSqlite = require('sqlite3');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
const pdfJs = require('pdfjs-dist');
|
||||
const { isAppleSilicon } = require('is-apple-silicon');
|
||||
require('@sentry/electron/renderer');
|
||||
@@ -39,8 +38,6 @@ require('@sentry/electron/renderer');
|
||||
// Allows components to use React as a global
|
||||
window.React = React;
|
||||
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
|
||||
|
||||
const main = async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -63,7 +60,7 @@ const main = async () => {
|
||||
BaseItem.loadClass('Revision', Revision);
|
||||
|
||||
Setting.setConstant('appId', bridge().appId());
|
||||
Setting.setConstant('appType', AppType.Desktop);
|
||||
Setting.setConstant('appType', 'desktop');
|
||||
Setting.setConstant('pluginAssetDir', `${__dirname}/pluginAssets`);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -109,7 +106,7 @@ const main = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
perfLogger.track('main', main).catch((error) => {
|
||||
main().catch((error) => {
|
||||
const env = bridge().env();
|
||||
console.error(error);
|
||||
|
||||
@@ -130,6 +127,6 @@ perfLogger.track('main', main).catch((error) => {
|
||||
// In dev, we give the option to leave the app open as debug statements in the
|
||||
// console can be useful
|
||||
const canIgnore = env === 'dev';
|
||||
void bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
|
||||
bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// This is the basic initialization for the Electron MAIN process
|
||||
|
||||
import './utils/sourceMapSetup';
|
||||
import { app as electronApp } from 'electron';
|
||||
require('./utils/sourceMapSetup');
|
||||
const electronApp = require('electron').app;
|
||||
require('@electron/remote/main').initialize();
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import { pathExistsSync, readFileSync, mkdirpSync } from 'fs-extra';
|
||||
import { initBridge } from './bridge';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
const ElectronAppWrapper = require('./ElectronAppWrapper').default;
|
||||
const { pathExistsSync, readFileSync, mkdirpSync } = require('fs-extra');
|
||||
const { initBridge } = require('./bridge');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
|
||||
const envFromArgs = require('@joplin/lib/envFromArgs');
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
|
||||
import registerCustomProtocols from './utils/customProtocols/registerCustomProtocols';
|
||||
const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils');
|
||||
const determineBaseAppDirs = require('@joplin/lib/determineBaseAppDirs').default;
|
||||
const registerCustomProtocols = require('./utils/customProtocols/registerCustomProtocols').default;
|
||||
|
||||
// Electron takes the application name from package.json `name` and
|
||||
// displays this in the tray icon toolip and message box titles, however in
|
||||
@@ -26,7 +26,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const getFlagValueFromArgs = (args: string[], flag: string, defaultValue: string|null) => {
|
||||
const getFlagValueFromArgs = (args, flag, defaultValue) => {
|
||||
if (!args) return null;
|
||||
const index = args.indexOf(flag);
|
||||
if (index <= 0 || index >= args.length - 1) return defaultValue;
|
||||
@@ -75,13 +75,7 @@ const wrapper = new ElectronAppWrapper(electronApp, {
|
||||
env, profilePath: rootProfileDir, isDebugMode, initialCallbackUrl, isEndToEndTesting,
|
||||
});
|
||||
|
||||
|
||||
type ExtendedGlobal = {
|
||||
joplinBridge: unknown;
|
||||
};
|
||||
(globalThis as unknown as ExtendedGlobal).joplinBridge = (
|
||||
initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId)
|
||||
);
|
||||
globalThis.joplinBridge = initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
|
||||
|
||||
wrapper.start().catch((error) => {
|
||||
console.error('Electron App fatal error:');
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.4.5",
|
||||
"version": "3.4.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -133,7 +133,7 @@
|
||||
"7zip-bin": "5.2.0",
|
||||
"@axe-core/playwright": "4.10.1",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.4",
|
||||
@@ -145,11 +145,11 @@
|
||||
"@playwright/test": "1.51.1",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.100",
|
||||
"@types/react": "18.3.21",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/mustache": "4.2.5",
|
||||
"@types/node": "18.19.86",
|
||||
"@types/react": "18.3.20",
|
||||
"@types/react-dom": "18.3.6",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
@@ -162,11 +162,11 @@
|
||||
"debounce": "1.2.1",
|
||||
"electron": "35.5.1",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-updater": "6.6.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"highlight.js": "11.11.1",
|
||||
"immer": "9.0.21",
|
||||
@@ -202,9 +202,9 @@
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.1.1",
|
||||
"tinymce": "6.8.5",
|
||||
"ts-jest": "29.3.1",
|
||||
"ts-jest": "29.1.5",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
|
||||
@@ -345,8 +345,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
return {
|
||||
id: result.commandName,
|
||||
title: result.title,
|
||||
parent_id: null as string,
|
||||
fields: [] as string[],
|
||||
parent_id: null,
|
||||
fields: [],
|
||||
type: BaseModel.TYPE_COMMAND,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -182,6 +182,12 @@ do
|
||||
fi
|
||||
done
|
||||
|
||||
echo '----------------------------------------------------'
|
||||
echo 'Running commands:'
|
||||
echo '';
|
||||
cat "$CMD_FILE"
|
||||
echo '----------------------------------------------------'
|
||||
|
||||
cd "$ROOT_DIR/packages/app-cli"
|
||||
yarn start --profile "$PROFILE_DIR" batch "$CMD_FILE"
|
||||
|
||||
|
||||
@@ -51,8 +51,10 @@
|
||||
const modulePath = args && args.length ? args[0] : null;
|
||||
if (!modulePath) throw new Error('No module path specified on `require` call');
|
||||
|
||||
// The sqlite3 is actually part of the lib package so we need to do
|
||||
// something convoluted to get it working.
|
||||
if (modulePath === 'sqlite3') {
|
||||
return require('sqlite3');
|
||||
return require('../../node_modules/@joplin/lib/node_modules/sqlite3/lib/sqlite3.js');
|
||||
}
|
||||
|
||||
if (modulePath === 'fs-extra') {
|
||||
|
||||
@@ -120,8 +120,8 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
|
||||
const bundleJs = async (writeStats: boolean) => {
|
||||
const entryPoints = [
|
||||
{ fileName: 'main.ts', renderer: false },
|
||||
{ fileName: 'main-html.ts', renderer: true },
|
||||
{ fileName: 'main.js', renderer: false },
|
||||
{ fileName: 'main-html.js', renderer: true },
|
||||
];
|
||||
for (const { fileName, renderer } of entryPoints) {
|
||||
const compiler = await makeBuildContext(fileName, renderer, writeStats);
|
||||
|
||||
3
packages/app-mobile/.gitignore
vendored
3
packages/app-mobile/.gitignore
vendored
@@ -67,8 +67,7 @@ yarn-error.log
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
/**/*.bundle.js
|
||||
/**/*.bundle.css
|
||||
components/**/*.bundle.js
|
||||
components/**/*.bundle.js.LICENSE.txt
|
||||
components/**/*.bundle.js.md5
|
||||
components/**/*.bundle.min.js
|
||||
|
||||
@@ -89,8 +89,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097777
|
||||
versionName "3.4.4"
|
||||
versionCode 2097773
|
||||
versionName "3.4.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
@@ -26,25 +20,4 @@ class MainActivity : ReactActivity() {
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
|
||||
|
||||
/**
|
||||
* This is a workaround to fix the upstream issue https://github.com/facebook/react-native/issues/49759#issuecomment-2918934967
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
val rootView = findViewById<View>(android.R.id.content)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||
val innerPadding = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
rootView.setPadding(
|
||||
innerPadding.left,
|
||||
innerPadding.top,
|
||||
innerPadding.right,
|
||||
innerPadding.bottom
|
||||
)
|
||||
insets
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ allprojects {
|
||||
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
|
||||
maven { url "https://maven.apache.org" }
|
||||
|
||||
maven {
|
||||
// Required by react-native-fingerprint-scanner
|
||||
// https://github.com/hieuvp/react-native-fingerprint-scanner/issues/192
|
||||
url "https://maven.aliyun.com/repository/jcenter"
|
||||
}
|
||||
|
||||
// Also required for react-native-vosk?
|
||||
maven { url "https://maven.google.com" }
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ pluginManagement {
|
||||
"../android/expo-gradle-plugin"
|
||||
).absolutePath
|
||||
includeBuild(expoPluginsPath)
|
||||
includeBuild("../node_modules/@react-native/gradle-plugin")
|
||||
}
|
||||
|
||||
plugins {
|
||||
|
||||
@@ -15,7 +15,6 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
await shim.fsDriver().writeFile(
|
||||
path,
|
||||
`<svg viewBox="0 -70 232 78" width="232" height="78" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="232" height="78" y="-70" rx="32" style="fill: #ccc;"/>
|
||||
<text style="font-family: serif; font-size: 104px; fill: rgb(128, 51, 128);">Test!</text>
|
||||
</svg>`,
|
||||
'utf8',
|
||||
|
||||
@@ -16,12 +16,24 @@ import useBarcodeScanner from './utils/useBarcodeScanner';
|
||||
import ScannedBarcodes from './ScannedBarcodes';
|
||||
import { CameraRef } from './Camera/types';
|
||||
import Camera from './Camera/index';
|
||||
import { CameraViewProps } from './types';
|
||||
import { CameraResult, OnInsertBarcode } from './types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useBackHandler from '../../utils/hooks/useBackHandler';
|
||||
|
||||
const logger = Logger.create('CameraView');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
cameraType: CameraDirection;
|
||||
cameraRatio: string;
|
||||
onPhoto: (data: CameraResult)=> void;
|
||||
// If null, cancelling should be handled by the parent
|
||||
// component
|
||||
onCancel: (()=> void)|null;
|
||||
onInsertBarcode: OnInsertBarcode;
|
||||
}
|
||||
|
||||
interface UseStyleProps {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
@@ -92,7 +104,7 @@ const useAvailableRatios = (): string[] => {
|
||||
};
|
||||
|
||||
|
||||
const CameraViewComponent: React.FC<CameraViewProps> = props => {
|
||||
const CameraViewComponent: React.FC<Props> = props => {
|
||||
const styles = useStyles(props);
|
||||
const cameraRef = useRef<CameraRef|null>(null);
|
||||
const [cameraReady, setCameraReady] = useState(false);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { PrimaryButton } from '../buttons';
|
||||
import { themeStyle } from '../global-style';
|
||||
import { CameraViewProps } from './types';
|
||||
import pickDocument from '../../utils/pickDocument';
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
const CameraViewComponent: React.FC<CameraViewProps> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
|
||||
const onUploadPress = useCallback(async () => {
|
||||
const response = await pickDocument({ preferCamera: true });
|
||||
for (const asset of response) {
|
||||
props.onPhoto({
|
||||
uri: asset.uri,
|
||||
type: asset.type,
|
||||
});
|
||||
}
|
||||
}, [props.onPhoto]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<PrimaryButton
|
||||
icon='file-upload'
|
||||
onPress={onUploadPress}
|
||||
>{_('Upload photo')}</PrimaryButton>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
cameraRatio: state.settings['camera.ratio'],
|
||||
cameraType: state.settings['camera.type'],
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CameraViewComponent);
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import CameraViewMultiPage from './CameraViewMultiPage';
|
||||
import CameraViewMultiPage, { OnComplete } from './CameraViewMultiPage';
|
||||
import { CameraResult, OnInsertBarcode } from './types';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
@@ -8,29 +8,24 @@ import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
||||
import { startCamera, takePhoto } from './utils/testing';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface WrapperProps {
|
||||
onComplete?: (finalPhotos: CameraResult[])=> void;
|
||||
onCancel?: ()=> void;
|
||||
onInsertBarcode?: OnInsertBarcode;
|
||||
onComplete?: OnComplete;
|
||||
}
|
||||
|
||||
let store: Store<AppState>;
|
||||
const WrappedCamera: React.FC<WrapperProps> = ({
|
||||
onCancel = jest.fn(),
|
||||
onComplete = jest.fn(),
|
||||
onInsertBarcode = jest.fn(),
|
||||
onCancel = jest.fn(),
|
||||
}) => {
|
||||
const [photos, setPhotos] = useState<CameraResult[]>([]);
|
||||
|
||||
return <TestProviderStack store={store}>
|
||||
<CameraViewMultiPage
|
||||
themeId={Setting.THEME_LIGHT}
|
||||
photos={photos}
|
||||
onSetPhotos={setPhotos}
|
||||
onCancel={onCancel}
|
||||
onComplete={() => onComplete(photos)}
|
||||
onComplete={onComplete}
|
||||
onInsertBarcode={onInsertBarcode}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { CameraResult } from './types';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, Platform, ImageBackground, ViewStyle, TextStyle } from 'react-native';
|
||||
import CameraView from './CameraView';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { themeStyle } from '../global-style';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { Button, Text } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import PhotoPreview from './PhotoPreview';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export type OnPhotosChange = (photos: CameraResult[])=> void;
|
||||
export type OnComplete = ()=> void;
|
||||
export type OnComplete = (photos: CameraResult[])=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
onCancel: ()=> void;
|
||||
onComplete: OnComplete;
|
||||
photos: CameraResult[];
|
||||
onSetPhotos: OnPhotosChange;
|
||||
onInsertBarcode: (barcodeText: string)=> void;
|
||||
}
|
||||
|
||||
@@ -44,8 +42,17 @@ const useStyle = (themeId: number) => {
|
||||
|
||||
imagePreview: {
|
||||
maxWidth: 70,
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
imageCountText: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
padding: 2,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
color: theme.color2,
|
||||
},
|
||||
@@ -53,13 +60,55 @@ const useStyle = (themeId: number) => {
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
interface PhotoProps {
|
||||
source: CameraResult;
|
||||
backgroundStyle: ViewStyle;
|
||||
textStyle: TextStyle;
|
||||
label: number;
|
||||
}
|
||||
|
||||
const PhotoPreview: React.FC<PhotoProps> = ({ source, label, backgroundStyle, textStyle }) => {
|
||||
const [uri, setUri] = useState('');
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (Platform.OS === 'web') {
|
||||
const file = await shim.fsDriver().fileAtPath(source.uri);
|
||||
if (event.cancelled) return;
|
||||
|
||||
const uri = URL.createObjectURL(file);
|
||||
setUri(uri);
|
||||
|
||||
event.onCleanup(() => {
|
||||
URL.revokeObjectURL(uri);
|
||||
});
|
||||
} else {
|
||||
setUri(source.uri);
|
||||
}
|
||||
}, [source]);
|
||||
return <ImageBackground
|
||||
style={backgroundStyle}
|
||||
resizeMode='contain'
|
||||
source={{ uri }}
|
||||
accessibilityLabel={_('%d photo(s) taken', label)}
|
||||
>
|
||||
<Text
|
||||
style={textStyle}
|
||||
testID='photo-count'
|
||||
>{label}</Text>
|
||||
</ImageBackground>;
|
||||
};
|
||||
|
||||
const CameraViewMultiPage: React.FC<Props> = ({
|
||||
onInsertBarcode, onCancel, onComplete, themeId, photos, onSetPhotos,
|
||||
onInsertBarcode, onCancel, onComplete, themeId,
|
||||
}) => {
|
||||
const [photos, setPhotos] = useState<CameraResult[]>([]);
|
||||
const onPhoto = useCallback((data: CameraResult) => {
|
||||
onSetPhotos([...photos, data]);
|
||||
}, [photos, onSetPhotos]);
|
||||
setPhotos(photos => [...photos, data]);
|
||||
}, []);
|
||||
|
||||
const onDonePressed = useCallback(() => {
|
||||
onComplete(photos);
|
||||
}, [photos, onComplete]);
|
||||
|
||||
const styles = useStyle(themeId);
|
||||
const renderLastPhoto = () => {
|
||||
@@ -88,7 +137,7 @@ const CameraViewMultiPage: React.FC<Props> = ({
|
||||
<Button
|
||||
icon='arrow-right'
|
||||
disabled={photos.length === 0}
|
||||
onPress={onComplete}
|
||||
onPress={onDonePressed}
|
||||
>{_('Next')}</Button>
|
||||
</View>
|
||||
</View>;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ViewStyle, TextStyle, Platform, ImageBackground, Text, StyleSheet } from 'react-native';
|
||||
import { useState } from 'react';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { CameraResult } from './types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface PhotoProps {
|
||||
source: CameraResult;
|
||||
backgroundStyle: ViewStyle;
|
||||
textStyle: TextStyle;
|
||||
label: number;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
background: {
|
||||
maxWidth: 70,
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
padding: 4,
|
||||
borderRadius: 32,
|
||||
color: 'white',
|
||||
backgroundColor: '#11c',
|
||||
},
|
||||
});
|
||||
|
||||
const PhotoPreview: React.FC<PhotoProps> = ({ source, label, backgroundStyle, textStyle }) => {
|
||||
const [uri, setUri] = useState('');
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (!source) {
|
||||
setUri('');
|
||||
} else if (Platform.OS === 'web') {
|
||||
const file = await shim.fsDriver().fileAtPath(source.uri);
|
||||
if (event.cancelled) return;
|
||||
|
||||
const uri = URL.createObjectURL(file);
|
||||
setUri(uri);
|
||||
|
||||
event.onCleanup(() => {
|
||||
URL.revokeObjectURL(uri);
|
||||
});
|
||||
} else {
|
||||
setUri(source.uri);
|
||||
}
|
||||
}, [source]);
|
||||
|
||||
return <ImageBackground
|
||||
style={[styles.background, backgroundStyle]}
|
||||
resizeMode='contain'
|
||||
source={{ uri }}
|
||||
accessibilityLabel={_('%d photo(s) taken', label)}
|
||||
>
|
||||
<Text
|
||||
style={[styles.text, textStyle]}
|
||||
testID='photo-count'
|
||||
>{label}</Text>
|
||||
</ImageBackground>;
|
||||
};
|
||||
|
||||
export default PhotoPreview;
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import type { ViewStyle } from 'react-native';
|
||||
|
||||
export type OnInsertBarcode = (barcodeText: string)=> void;
|
||||
|
||||
@@ -7,15 +5,3 @@ export interface CameraResult {
|
||||
uri: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface CameraViewProps {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
cameraType: CameraDirection;
|
||||
cameraRatio: string;
|
||||
onPhoto: (data: CameraResult)=> void;
|
||||
// If null, cancelling should be handled by the parent
|
||||
// component
|
||||
onCancel: (()=> void)|null;
|
||||
onInsertBarcode: OnInsertBarcode;
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import createMockReduxStore from '../utils/testing/createMockReduxStore';
|
||||
import TestProviderStack from './testing/TestProviderStack';
|
||||
import ComboBox, { OnItemSelected, Option } from './ComboBox';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface Item {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
items: Item[];
|
||||
onItemSelected?: OnItemSelected;
|
||||
}
|
||||
|
||||
const store = createMockReduxStore();
|
||||
const WrappedComboBox: React.FC<WrapperProps> = ({
|
||||
items,
|
||||
onItemSelected = jest.fn(),
|
||||
}: WrapperProps) => {
|
||||
const mappedItems = useMemo(() => {
|
||||
return items.map((item): Option => ({
|
||||
title: item.title,
|
||||
icon: undefined,
|
||||
accessibilityHint: undefined,
|
||||
willRemoveOnPress: false,
|
||||
}));
|
||||
}, [items]);
|
||||
|
||||
return <TestProviderStack store={store}>
|
||||
<ComboBox
|
||||
items={mappedItems}
|
||||
alwaysExpand={true}
|
||||
style={{}}
|
||||
onItemSelected={onItemSelected}
|
||||
placeholder={'Test combobox'}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const getSearchInput = () => {
|
||||
return screen.getByPlaceholderText('Test combobox');
|
||||
};
|
||||
const getSearchResults = () => {
|
||||
return screen.getAllByTestId(/^search-result-/);
|
||||
};
|
||||
|
||||
describe('ComboBox', () => {
|
||||
test('should list all items when the search query is empty', () => {
|
||||
const testItems = [
|
||||
{ title: 'test 1' },
|
||||
{ title: 'test 2' },
|
||||
{ title: 'test 3' },
|
||||
];
|
||||
const { unmount } = render(
|
||||
<WrappedComboBox
|
||||
items={testItems}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getSearchInput()).toHaveTextContent('');
|
||||
expect(getSearchResults()).toHaveLength(3);
|
||||
|
||||
// Manually unmounting prevents a warning
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('changing the search query should limit which items are visible', () => {
|
||||
const testItems = [
|
||||
{ title: 'a' },
|
||||
{ title: 'b' },
|
||||
{ title: 'c' },
|
||||
{ title: 'aa' },
|
||||
];
|
||||
const { unmount } = render(
|
||||
<WrappedComboBox items={testItems}/>,
|
||||
);
|
||||
|
||||
expect(getSearchResults()).toHaveLength(4);
|
||||
fireEvent.changeText(getSearchInput(), 'a');
|
||||
|
||||
const updatedResults = getSearchResults();
|
||||
expect(updatedResults[0]).toHaveTextContent('a');
|
||||
expect(updatedResults[1]).toHaveTextContent('aa');
|
||||
expect(updatedResults).toHaveLength(2);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -1,601 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { AccessibilityInfo, NativeSyntheticEvent, Platform, Role, ScrollViewProps, StyleSheet, TextInput, TextInputProps, useWindowDimensions, View, ViewProps, ViewStyle } from 'react-native';
|
||||
import { TouchableRipple, Text } from 'react-native-paper';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../utils/types';
|
||||
import { themeStyle } from './global-style';
|
||||
import Icon from './Icon';
|
||||
import { RefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import SearchInput from './SearchInput';
|
||||
import focusView from '../utils/focusView';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import NestableFlatList, { NestableFlatListControl } from './NestableFlatList';
|
||||
const naturalCompare = require('string-natural-compare');
|
||||
|
||||
|
||||
export interface Option {
|
||||
title: string;
|
||||
icon: string|undefined;
|
||||
accessibilityHint: string|undefined;
|
||||
onPress?: ()=> void;
|
||||
|
||||
// True if pressing this option removes it. Used for working around
|
||||
// focus issues.
|
||||
willRemoveOnPress: boolean;
|
||||
}
|
||||
|
||||
export type OnItemSelected = (item: Option, index: number)=> void;
|
||||
|
||||
interface BaseProps {
|
||||
themeId: number;
|
||||
items: Option[];
|
||||
alwaysExpand: boolean;
|
||||
placeholder: string;
|
||||
onItemSelected: OnItemSelected;
|
||||
style: ViewStyle;
|
||||
searchInputProps?: TextInputProps;
|
||||
searchResultProps?: ScrollViewProps;
|
||||
}
|
||||
|
||||
type OnAddItem = (content: string)=> void;
|
||||
type OnCanAddItem = (item: string)=> boolean;
|
||||
|
||||
type Props = BaseProps & ({
|
||||
onAddItem: OnAddItem|null;
|
||||
canAddItem: OnCanAddItem;
|
||||
}|{
|
||||
onAddItem?: undefined;
|
||||
canAddItem?: undefined;
|
||||
});
|
||||
|
||||
const optionKeyExtractor = (option: Option) => option.title;
|
||||
|
||||
interface UseSearchResultsOptions {
|
||||
search: string;
|
||||
setSearch: (search: string)=> void;
|
||||
|
||||
options: Option[];
|
||||
onAddItem: null|OnAddItem;
|
||||
canAddItem: OnCanAddItem;
|
||||
}
|
||||
|
||||
const useSearchResults = ({
|
||||
search, setSearch, options, onAddItem, canAddItem,
|
||||
}: UseSearchResultsOptions) => {
|
||||
const results = useMemo(() => {
|
||||
return options
|
||||
.filter(option => option.title.startsWith(search))
|
||||
.sort((a, b) => {
|
||||
if (a.title === b.title) return 0;
|
||||
// Full matches should go first
|
||||
if (a.title === search) return -1;
|
||||
if (b.title === search) return 1;
|
||||
return naturalCompare(a.title, b.title);
|
||||
});
|
||||
}, [search, options]);
|
||||
|
||||
const canAdd = (
|
||||
!!onAddItem
|
||||
&& search.trim()
|
||||
&& results[0]?.title !== search
|
||||
&& canAddItem(search)
|
||||
);
|
||||
|
||||
// Use a ref to prevent unnecessary rerenders if onAddItem changes
|
||||
const addCurrentSearch = useRef(()=>{});
|
||||
addCurrentSearch.current = () => {
|
||||
onAddItem(search);
|
||||
AccessibilityInfo.announceForAccessibility(_('Added new: %s', search));
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
return useMemo(() => {
|
||||
if (!canAdd) return results;
|
||||
|
||||
return [
|
||||
...results,
|
||||
{
|
||||
title: _('Add new'),
|
||||
icon: 'fas fa-plus',
|
||||
accessibilityHint: undefined,
|
||||
willRemoveOnPress: true,
|
||||
onPress: () => {
|
||||
addCurrentSearch.current?.();
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [canAdd, results]);
|
||||
};
|
||||
|
||||
interface SelectedIndexControl {
|
||||
onNextResult: ()=> void;
|
||||
onPreviousResult: ()=> void;
|
||||
onFirstResult: ()=> void;
|
||||
onLastResult: ()=> void;
|
||||
}
|
||||
|
||||
const useSelectedIndex = (search: string, searchResults: Option[]) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
setSelectedIndex(0);
|
||||
} else {
|
||||
const hasResults = !!searchResults.length;
|
||||
setSelectedIndex(hasResults ? 0 : -1);
|
||||
}
|
||||
}, [searchResults, search]);
|
||||
|
||||
const resultCount = searchResults.length;
|
||||
const selectedIndexControl: SelectedIndexControl = useMemo(() => ({
|
||||
onNextResult: () => {
|
||||
setSelectedIndex(index => {
|
||||
return Math.min(index + 1, resultCount - 1);
|
||||
});
|
||||
},
|
||||
onPreviousResult: () => {
|
||||
setSelectedIndex(index => {
|
||||
return Math.max(index - 1, 0);
|
||||
});
|
||||
},
|
||||
onFirstResult: () => {
|
||||
setSelectedIndex(0);
|
||||
},
|
||||
onLastResult: () => {
|
||||
setSelectedIndex(resultCount - 1);
|
||||
},
|
||||
}), [resultCount]);
|
||||
|
||||
return { selectedIndex, selectedIndexControl };
|
||||
};
|
||||
|
||||
const useStyles = (themeId: number, showSearchResults: boolean) => {
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const menuItemHeight = 40 * fontScale;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const borderRadius = 4;
|
||||
const itemMarginVertical = 8;
|
||||
return StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
|
||||
borderRadius,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderColor: theme.dividerColor,
|
||||
borderWidth: showSearchResults ? 1 : 0,
|
||||
},
|
||||
searchInputContainer: {
|
||||
borderRadius,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderColor: theme.dividerColor,
|
||||
borderWidth: 1,
|
||||
...(showSearchResults ? {
|
||||
borderTopWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
} : {}),
|
||||
},
|
||||
tagSearchHelp: {
|
||||
color: theme.colorFaded,
|
||||
marginTop: 6,
|
||||
},
|
||||
searchInput: {
|
||||
minHeight: 32,
|
||||
},
|
||||
searchResults: {
|
||||
height: 200,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
...(showSearchResults ? {} : {
|
||||
display: 'none',
|
||||
}),
|
||||
},
|
||||
optionIcon: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
textAlign: 'center',
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
},
|
||||
optionLabel: {
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
paddingInlineStart: 3,
|
||||
},
|
||||
optionContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius,
|
||||
|
||||
height: menuItemHeight - itemMarginVertical,
|
||||
marginTop: itemMarginVertical / 2,
|
||||
marginBottom: itemMarginVertical / 2,
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
optionContentSelected: {
|
||||
backgroundColor: theme.selectedColor,
|
||||
},
|
||||
});
|
||||
}, [theme, menuItemHeight, showSearchResults]);
|
||||
|
||||
return { menuItemHeight, styles };
|
||||
};
|
||||
|
||||
type Styles = ReturnType<typeof useStyles>['styles'];
|
||||
|
||||
interface SearchResultProps {
|
||||
text: string;
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
styles: Styles;
|
||||
}
|
||||
|
||||
const SearchResult: React.FC<SearchResultProps> = ({
|
||||
text, styles, selected, icon: iconName,
|
||||
}) => {
|
||||
const icon = iconName ? <Icon
|
||||
style={styles.optionIcon}
|
||||
name={iconName}
|
||||
// Description is provided by adjacent text
|
||||
accessibilityLabel={null}
|
||||
/> : null;
|
||||
|
||||
return (
|
||||
<View style={[styles.optionContent, selected && styles.optionContentSelected]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={styles.optionLabel}
|
||||
>{text}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface ResultWrapperProps extends ViewProps {
|
||||
index: number;
|
||||
item: Option;
|
||||
}
|
||||
|
||||
interface SearchResultContainerProps {
|
||||
onItemSelected: OnItemSelected;
|
||||
selectedIndex: number;
|
||||
baseId: string;
|
||||
resultCount: number;
|
||||
searchInputRef: RefObject<TextInput>;
|
||||
// Used to determine focus
|
||||
resultsHideOnPress: boolean;
|
||||
}
|
||||
|
||||
const useSearchResultContainerComponent = ({
|
||||
onItemSelected, selectedIndex, baseId, resultCount, searchInputRef, resultsHideOnPress,
|
||||
}: SearchResultContainerProps): React.FC<ResultWrapperProps> => {
|
||||
const listItemsRef = useRef<Record<number, View>>({});
|
||||
|
||||
const eventQueue = useMemo(() => {
|
||||
const queue = new AsyncActionQueue(100);
|
||||
// Don't allow skipping any onItemSelected calls:
|
||||
queue.setCanSkipTaskHandler(() => false);
|
||||
return queue;
|
||||
}, []);
|
||||
const onItemPressRef = useRef(onItemSelected);
|
||||
onItemPressRef.current = (item, index) => {
|
||||
let focusTarget = null;
|
||||
|
||||
if (resultsHideOnPress) {
|
||||
focusTarget = searchInputRef.current;
|
||||
} else if (Platform.OS === 'android' && item.willRemoveOnPress) {
|
||||
// Workaround for an accessibility bug on Android: By default, when an item is removed
|
||||
// from the list of results, focus can occasionally jump to the start of the document.
|
||||
// To prevent this, manually move focus to the next item before the results list changes:
|
||||
const adjacentView = listItemsRef.current[index + 1] ?? listItemsRef.current[index - 1];
|
||||
|
||||
focusTarget = adjacentView ?? searchInputRef.current;
|
||||
}
|
||||
|
||||
if (focusTarget) {
|
||||
focusView('ComboBox::focusAfterPress', focusTarget);
|
||||
|
||||
eventQueue.push(() => {
|
||||
onItemSelected(item, index);
|
||||
});
|
||||
} else {
|
||||
onItemSelected(item, index);
|
||||
}
|
||||
};
|
||||
|
||||
// For the correct accessibility structure, the `TouchableRipple`s need to be siblings.
|
||||
return useMemo(() => ({ index, item, children, ...rest }) => (
|
||||
<TouchableRipple
|
||||
{...rest}
|
||||
ref={(item) => {
|
||||
listItemsRef.current[index] = item;
|
||||
}}
|
||||
onPress={() => { onItemPressRef.current(item, index); }}
|
||||
// On web, focus is controlled using the arrow keys. On other
|
||||
// platforms, arrow key navigation is not available and each item
|
||||
// needs to be focusable
|
||||
tabIndex={Platform.OS === 'web' ? -1 : undefined}
|
||||
role={Platform.OS === 'web' ? 'option' : 'button'}
|
||||
accessibilityHint={item.accessibilityHint}
|
||||
aria-selected={index === selectedIndex}
|
||||
nativeID={`${baseId}-${index}`}
|
||||
testID={`search-result-${index}`}
|
||||
aria-setsize={resultCount}
|
||||
aria-posinset={index + 1}
|
||||
><View>{children}</View></TouchableRipple>
|
||||
), [selectedIndex, baseId, resultCount]);
|
||||
};
|
||||
|
||||
const useShowSearchResults = (alwaysExpand: boolean, search: string) => {
|
||||
const [showSearchResults, setShowSearchResults] = useState(alwaysExpand);
|
||||
|
||||
const showResultsRef = useRef(showSearchResults);
|
||||
showResultsRef.current = showSearchResults;
|
||||
|
||||
useEffect(() => {
|
||||
if (alwaysExpand) {
|
||||
setShowSearchResults(true);
|
||||
}
|
||||
}, [alwaysExpand]);
|
||||
|
||||
useEffect(() => {
|
||||
if (search.length > 0 && !showResultsRef.current) {
|
||||
setShowSearchResults(true);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
return { showSearchResults, setShowSearchResults };
|
||||
};
|
||||
|
||||
interface AnnounceSelectionOptions {
|
||||
enabled: boolean;
|
||||
selectedResultTitle: string|undefined;
|
||||
resultCount: number;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
const useAnnounceSelection = ({ selectedResultTitle, resultCount, enabled, searchQuery }: AnnounceSelectionOptions) => {
|
||||
const enabledRef = useRef(enabled);
|
||||
enabledRef.current = enabled;
|
||||
|
||||
const announcement = (() => {
|
||||
if (!searchQuery) return '';
|
||||
if (resultCount === 0) return _('No results');
|
||||
if (selectedResultTitle) return _('Selected: %s', selectedResultTitle);
|
||||
return '';
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (enabledRef.current && announcement) {
|
||||
AccessibilityInfo.announceForAccessibility(announcement);
|
||||
}
|
||||
}, [announcement]);
|
||||
};
|
||||
|
||||
const useSelectionAutoScroll = (
|
||||
listRef: RefObject<NestableFlatListControl|null>, results: Option[], selectedIndex: number,
|
||||
) => {
|
||||
const resultsRef = useRef(results);
|
||||
resultsRef.current = results;
|
||||
useEffect(() => {
|
||||
if (resultsRef.current?.length && selectedIndex >= 0) {
|
||||
listRef.current?.scrollToIndex({ index: selectedIndex, animated: false, viewPosition: 0.4 });
|
||||
}
|
||||
}, [selectedIndex, listRef]);
|
||||
};
|
||||
|
||||
interface UseInputEventHandlersProps {
|
||||
selectedIndexControl: SelectedIndexControl;
|
||||
onItemSelected: OnItemSelected;
|
||||
|
||||
selectedIndex: number;
|
||||
selectedResult: Option|null;
|
||||
alwaysExpand: boolean;
|
||||
showSearchResults: boolean;
|
||||
setShowSearchResults: (show: boolean)=> void;
|
||||
setSearch: (search: string)=> void;
|
||||
}
|
||||
|
||||
const useInputEventHandlers = ({
|
||||
selectedIndexControl,
|
||||
onItemSelected: propsOnItemSelected, setShowSearchResults, alwaysExpand,
|
||||
setSearch, selectedResult, selectedIndex, showSearchResults,
|
||||
}: UseInputEventHandlersProps) => {
|
||||
|
||||
const propsOnItemSelectedRef = useRef(propsOnItemSelected);
|
||||
propsOnItemSelectedRef.current = propsOnItemSelected;
|
||||
|
||||
const onItemSelected = useCallback((item: Option, index: number) => {
|
||||
let result;
|
||||
if (item.onPress) {
|
||||
result = item.onPress();
|
||||
} else {
|
||||
result = propsOnItemSelectedRef.current(item, index);
|
||||
}
|
||||
|
||||
if (!alwaysExpand) {
|
||||
setSearch('');
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [setShowSearchResults, alwaysExpand, setSearch]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (selectedResult) {
|
||||
onItemSelected(selectedResult, selectedIndex);
|
||||
setSearch('');
|
||||
}
|
||||
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
|
||||
|
||||
// For now, onKeyPress only works on web.
|
||||
// See https://github.com/react-native-community/discussions-and-proposals/issues/249
|
||||
type KeyPressEvent = { key: string };
|
||||
const onKeyPress = useCallback((event: NativeSyntheticEvent<KeyPressEvent>) => {
|
||||
const key = event.nativeEvent.key;
|
||||
const isDownArrow = key === 'ArrowDown';
|
||||
const isUpArrow = key === 'ArrowUp';
|
||||
if (!showSearchResults && (isDownArrow || isUpArrow)) {
|
||||
setShowSearchResults(true);
|
||||
if (isUpArrow) {
|
||||
selectedIndexControl.onLastResult();
|
||||
} else {
|
||||
selectedIndexControl.onFirstResult();
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (key === 'ArrowDown') {
|
||||
selectedIndexControl.onNextResult();
|
||||
event.preventDefault();
|
||||
} else if (key === 'ArrowUp') {
|
||||
selectedIndexControl.onPreviousResult();
|
||||
event.preventDefault();
|
||||
} else if (key === 'Enter') {
|
||||
// This case is necessary on web to prevent the
|
||||
// search input from becoming defocused after
|
||||
// pressing "enter".
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
setSearch('');
|
||||
} else if (key === 'Escape' && !alwaysExpand) {
|
||||
setShowSearchResults(false);
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [onSubmit, setSearch, selectedIndexControl, setShowSearchResults, showSearchResults, alwaysExpand]);
|
||||
|
||||
return { onKeyPress, onItemSelected, onSubmit };
|
||||
};
|
||||
|
||||
|
||||
const ComboBox: React.FC<Props> = ({
|
||||
themeId,
|
||||
items,
|
||||
onItemSelected: propsOnItemSelected,
|
||||
placeholder,
|
||||
onAddItem,
|
||||
canAddItem,
|
||||
style: rootStyle,
|
||||
alwaysExpand,
|
||||
searchInputProps,
|
||||
searchResultProps,
|
||||
}) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const { showSearchResults, setShowSearchResults } = useShowSearchResults(alwaysExpand, search);
|
||||
const { styles, menuItemHeight } = useStyles(themeId, showSearchResults);
|
||||
|
||||
const results = useSearchResults({
|
||||
search,
|
||||
setSearch,
|
||||
options: items,
|
||||
onAddItem,
|
||||
canAddItem,
|
||||
});
|
||||
const { selectedIndex, selectedIndexControl } = useSelectedIndex(search, results);
|
||||
const searchInputRef = useRef<TextInput|null>(null);
|
||||
const listRef = useRef<NestableFlatListControl|null>(null);
|
||||
|
||||
useSelectionAutoScroll(listRef, results, selectedIndex);
|
||||
|
||||
useAnnounceSelection({
|
||||
// On web, announcements are handled natively based on accessibility roles.
|
||||
// Manual announcements are only needed on iOS and Android:
|
||||
enabled: Platform.OS !== 'web',
|
||||
selectedResultTitle: results[selectedIndex]?.title,
|
||||
searchQuery: search,
|
||||
resultCount: results.length,
|
||||
});
|
||||
|
||||
const { onItemSelected, onKeyPress, onSubmit } = useInputEventHandlers({
|
||||
selectedIndexControl,
|
||||
onItemSelected: propsOnItemSelected,
|
||||
|
||||
selectedIndex,
|
||||
selectedResult: results[selectedIndex],
|
||||
alwaysExpand,
|
||||
showSearchResults,
|
||||
setShowSearchResults,
|
||||
setSearch,
|
||||
});
|
||||
|
||||
const baseId = useId();
|
||||
const SearchResultWrapper = useSearchResultContainerComponent({
|
||||
onItemSelected, selectedIndex, baseId, searchInputRef, resultCount: results.length,
|
||||
resultsHideOnPress: !alwaysExpand,
|
||||
});
|
||||
|
||||
type RenderEvent = { item: Option; index: number };
|
||||
const renderItem = useCallback(({ item, index }: RenderEvent) => {
|
||||
return <SearchResult
|
||||
text={item.title}
|
||||
styles={styles}
|
||||
selected={index === selectedIndex}
|
||||
icon={item.icon ?? ''}
|
||||
/>;
|
||||
}, [selectedIndex, styles]);
|
||||
|
||||
const webProps = {
|
||||
onKeyDown: onKeyPress,
|
||||
};
|
||||
const activeId = `${baseId}-${selectedIndex}`;
|
||||
const searchResults = <NestableFlatList
|
||||
ref={listRef}
|
||||
data={results}
|
||||
{...searchResultProps}
|
||||
|
||||
CellRendererComponent={SearchResultWrapper}
|
||||
itemHeight={menuItemHeight}
|
||||
|
||||
contentWrapperProps={{
|
||||
// A better role would be 'listbox', but that isn't supported by RN.
|
||||
role: Platform.OS === 'web' ? 'listbox' as Role : undefined,
|
||||
'aria-activedescendant': activeId,
|
||||
nativeID: `menuBox-${baseId}`,
|
||||
onKeyPress,
|
||||
// Allow focusing the results list directly on web. It has been observed
|
||||
// that certain screen readers on web sometimes fail to read changes to the results list.
|
||||
// Being able to navigate directly to the results list may help users in this case.
|
||||
tabIndex: Platform.OS === 'web' ? 0 : undefined,
|
||||
} as ViewProps}
|
||||
|
||||
style={styles.searchResults}
|
||||
keyExtractor={optionKeyExtractor}
|
||||
extraData={renderItem}
|
||||
renderItem={renderItem}
|
||||
/>;
|
||||
|
||||
const helpComponent = <Text style={styles.tagSearchHelp}>{_('To create a new tag, type the name and press enter.')}</Text>;
|
||||
|
||||
return <View style={[styles.root, rootStyle]} {...webProps}>
|
||||
<SearchInput
|
||||
inputRef={searchInputRef}
|
||||
themeId={themeId}
|
||||
containerStyle={styles.searchInputContainer}
|
||||
style={styles.searchInput}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
onKeyPress={onKeyPress}
|
||||
onSubmitEditing={onSubmit}
|
||||
placeholder={placeholder}
|
||||
aria-activedescendant={showSearchResults ? activeId : undefined}
|
||||
aria-controls={`menuBox-${baseId}`}
|
||||
|
||||
// Certain accessibility properties only work well on web:
|
||||
{...(Platform.OS === 'web' ? {
|
||||
role: 'combobox',
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-expanded': showSearchResults,
|
||||
'aria-label': placeholder,
|
||||
} : {})}
|
||||
{...searchInputProps}
|
||||
/>
|
||||
{searchResults}
|
||||
{!showSearchResults && helpComponent}
|
||||
</View>;
|
||||
};
|
||||
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
themeId: state.settings.theme,
|
||||
}))(ComboBox);
|
||||
@@ -1,19 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { Dialog, Divider, Surface, Text } from 'react-native-paper';
|
||||
import { DialogType, ButtonDialogData } from './types';
|
||||
import { StyleSheet, ViewStyle } from 'react-native';
|
||||
import { DialogType, PromptDialogData } from './types';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useMemo } from 'react';
|
||||
import { themeStyle } from '../global-style';
|
||||
import PromptButton from './PromptButton';
|
||||
|
||||
interface Props {
|
||||
dialog: ButtonDialogData;
|
||||
containerStyle: ViewStyle;
|
||||
dialog: PromptDialogData;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const useStyles = (isMenu: boolean) => {
|
||||
const useStyles = (themeId: number, isMenu: boolean) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
dialogContainer: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderRadius: theme.borderRadius,
|
||||
paddingTop: theme.borderRadius,
|
||||
marginLeft: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
|
||||
dialogContent: {
|
||||
paddingBottom: 14,
|
||||
@@ -31,12 +40,12 @@ const useStyles = (isMenu: boolean) => {
|
||||
textAlign: isMenu ? 'center' : undefined,
|
||||
},
|
||||
});
|
||||
}, [isMenu]);
|
||||
}, [themeId, isMenu]);
|
||||
};
|
||||
|
||||
const PromptDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
|
||||
const PromptDialog: React.FC<Props> = ({ dialog, themeId }) => {
|
||||
const isMenu = dialog.type === DialogType.Menu;
|
||||
const styles = useStyles(isMenu);
|
||||
const styles = useStyles(themeId, isMenu);
|
||||
|
||||
const buttons = dialog.buttons.map((button, index) => {
|
||||
return <PromptButton
|
||||
@@ -54,7 +63,7 @@ const PromptDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
|
||||
return (
|
||||
<Surface
|
||||
testID={'prompt-dialog'}
|
||||
style={containerStyle}
|
||||
style={styles.dialogContainer}
|
||||
key={dialog.key}
|
||||
elevation={1}
|
||||
>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Dialog, Surface, Text } from 'react-native-paper';
|
||||
import { TextInputDialogData } from './types';
|
||||
import { StyleSheet, ViewStyle } from 'react-native';
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
import PromptButton from './PromptButton';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import TextInput from '../TextInput';
|
||||
|
||||
interface Props {
|
||||
dialog: TextInputDialogData;
|
||||
containerStyle: ViewStyle;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const useStyles = () => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
dialogContent: {
|
||||
paddingBottom: 14,
|
||||
},
|
||||
dialogActions: {
|
||||
paddingBottom: 14,
|
||||
paddingTop: 4,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
|
||||
const styles = useStyles();
|
||||
const [text, setText] = useState('');
|
||||
const labelId = useId();
|
||||
|
||||
return (
|
||||
<Surface
|
||||
testID={'prompt-dialog'}
|
||||
style={containerStyle}
|
||||
key={dialog.key}
|
||||
elevation={1}
|
||||
>
|
||||
<Dialog.Content style={styles.dialogContent}>
|
||||
<Text
|
||||
variant='bodyMedium'
|
||||
nativeID={labelId}
|
||||
>{dialog.message}</Text>
|
||||
<TextInput
|
||||
aria-labelledby={labelId}
|
||||
themeId={themeId}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions
|
||||
style={styles.dialogActions}
|
||||
>
|
||||
<PromptButton
|
||||
buttonSpec={{
|
||||
text: _('Cancel'),
|
||||
onPress: dialog.onDismiss,
|
||||
}}
|
||||
themeId={themeId}
|
||||
/>
|
||||
<PromptButton
|
||||
buttonSpec={{
|
||||
text: _('OK'),
|
||||
onPress: () => dialog.onSubmit(text),
|
||||
}}
|
||||
themeId={themeId}
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</Surface>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInputDialog;
|
||||
@@ -1,16 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, Platform } from 'react-native';
|
||||
import { DialogControl, DialogType, MenuChoice, PromptButtonSpec, DialogData, PromptOptions } from '../types';
|
||||
import { DialogControl, DialogType, MenuChoice, PromptButtonSpec, PromptDialogData, PromptOptions } from '../types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
type SetPromptDialogs = React.Dispatch<React.SetStateAction<DialogData[]>>;
|
||||
type SetPromptDialogs = React.Dispatch<React.SetStateAction<PromptDialogData[]>>;
|
||||
|
||||
const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
const nextDialogIdRef = useRef(0);
|
||||
|
||||
const dialogControl: DialogControl = useMemo(() => {
|
||||
const onDismiss = (dialog: DialogData) => {
|
||||
const onDismiss = (dialog: PromptDialogData) => {
|
||||
setPromptDialogs(dialogs => dialogs.filter(d => d !== dialog));
|
||||
};
|
||||
|
||||
@@ -39,8 +39,8 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
Alert.alert(title, message, buttons, options);
|
||||
} else {
|
||||
const cancelable = options?.cancelable ?? true;
|
||||
const dialog: DialogData = {
|
||||
type: DialogType.ButtonPrompt,
|
||||
const dialog: PromptDialogData = {
|
||||
type: DialogType.Prompt,
|
||||
key: `dialog-${nextDialogIdRef.current++}`,
|
||||
title,
|
||||
message,
|
||||
@@ -69,7 +69,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
return new Promise<T>((resolve) => {
|
||||
const dismiss = () => onDismiss(dialog);
|
||||
|
||||
const dialog: DialogData = {
|
||||
const dialog: PromptDialogData = {
|
||||
type: DialogType.Menu,
|
||||
key: `menu-dialog-${nextDialogIdRef.current++}`,
|
||||
title: '',
|
||||
@@ -91,33 +91,6 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
});
|
||||
});
|
||||
},
|
||||
promptForText: (message: string) => {
|
||||
return new Promise<string|null>((resolve) => {
|
||||
const dismiss = () => {
|
||||
onDismiss(dialog);
|
||||
};
|
||||
|
||||
const dialog: DialogData = {
|
||||
type: DialogType.TextInput,
|
||||
key: `prompt-dialog-${nextDialogIdRef.current++}`,
|
||||
message,
|
||||
onSubmit: (text) => {
|
||||
resolve(text);
|
||||
dismiss();
|
||||
},
|
||||
onDismiss: () => {
|
||||
resolve(null);
|
||||
dismiss();
|
||||
},
|
||||
};
|
||||
setPromptDialogs(dialogs => {
|
||||
return [
|
||||
...dialogs,
|
||||
dialog,
|
||||
];
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return control;
|
||||
|
||||
@@ -5,11 +5,10 @@ import { Portal } from 'react-native-paper';
|
||||
import Modal from '../Modal';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import makeShowMessageBox from '../../utils/makeShowMessageBox';
|
||||
import { DialogControl, DialogData, DialogType } from './types';
|
||||
import { DialogControl, PromptDialogData } from './types';
|
||||
import useDialogControl from './hooks/useDialogControl';
|
||||
import PromptDialog from './PromptDialog';
|
||||
import { themeStyle } from '../global-style';
|
||||
import TextInputDialog from './TextInputDialog';
|
||||
|
||||
export type { DialogControl } from './types';
|
||||
export const DialogContext = createContext<DialogControl>(null);
|
||||
@@ -19,11 +18,10 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
const useStyles = () => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
modalContainer: {
|
||||
marginLeft: 'auto',
|
||||
@@ -33,20 +31,12 @@ const useStyles = (themeId: number) => {
|
||||
width: Math.max(windowSize.width / 2, 400),
|
||||
maxWidth: '100%',
|
||||
},
|
||||
|
||||
dialogContainer: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderRadius: theme.borderRadius,
|
||||
paddingTop: theme.borderRadius,
|
||||
marginLeft: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
}, [windowSize.width, themeId]);
|
||||
}, [windowSize.width]);
|
||||
};
|
||||
|
||||
const DialogManager: React.FC<Props> = props => {
|
||||
const [dialogModels, setPromptDialogs] = useState<DialogData[]>([]);
|
||||
const [dialogModels, setPromptDialogs] = useState<PromptDialogData[]>([]);
|
||||
|
||||
const dialogControl = useDialogControl(setPromptDialogs);
|
||||
const dialogControlRef = useRef(dialogControl);
|
||||
@@ -61,34 +51,17 @@ const DialogManager: React.FC<Props> = props => {
|
||||
}, []);
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = useStyles(props.themeId);
|
||||
const styles = useStyles();
|
||||
|
||||
const dialogComponents: React.ReactNode[] = [];
|
||||
for (const dialog of dialogModels) {
|
||||
const dialogProps = {
|
||||
containerStyle: styles.dialogContainer,
|
||||
themeId: props.themeId,
|
||||
};
|
||||
if (dialog.type === DialogType.Menu || dialog.type === DialogType.ButtonPrompt) {
|
||||
dialogComponents.push(
|
||||
<PromptDialog
|
||||
dialog={dialog}
|
||||
{...dialogProps}
|
||||
key={dialog.key}
|
||||
/>,
|
||||
);
|
||||
} else if (dialog.type === DialogType.TextInput) {
|
||||
dialogComponents.push(
|
||||
<TextInputDialog
|
||||
dialog={dialog}
|
||||
{...dialogProps}
|
||||
key={dialog.key}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
const exhaustivenessCheck: never = dialog.type;
|
||||
throw new Error(`Unexpected dialog type ${exhaustivenessCheck}`);
|
||||
}
|
||||
dialogComponents.push(
|
||||
<PromptDialog
|
||||
key={dialog.key}
|
||||
dialog={dialog}
|
||||
themeId={props.themeId}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
// Web: Use a <Modal> wrapper for better keyboard focus handling.
|
||||
|
||||
@@ -24,18 +24,16 @@ export interface DialogControl {
|
||||
info(message: string): Promise<void>;
|
||||
error(message: string): Promise<void>;
|
||||
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
|
||||
promptForText(message: string): Promise<string>;
|
||||
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
|
||||
}
|
||||
|
||||
export enum DialogType {
|
||||
ButtonPrompt,
|
||||
Prompt,
|
||||
Menu,
|
||||
TextInput,
|
||||
}
|
||||
|
||||
export interface ButtonDialogData {
|
||||
type: DialogType.ButtonPrompt|DialogType.Menu;
|
||||
export interface PromptDialogData {
|
||||
type: DialogType;
|
||||
key: string;
|
||||
title: string;
|
||||
message: string;
|
||||
@@ -43,13 +41,3 @@ export interface ButtonDialogData {
|
||||
onDismiss: (()=> void)|null;
|
||||
}
|
||||
|
||||
export interface TextInputDialogData {
|
||||
type: DialogType.TextInput;
|
||||
key: string;
|
||||
message: string;
|
||||
onSubmit: (text: string)=> void;
|
||||
onDismiss: ()=> void;
|
||||
}
|
||||
|
||||
export type DialogData = ButtonDialogData | TextInputDialogData;
|
||||
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef,
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useMemo, useRef,
|
||||
} from 'react';
|
||||
|
||||
import { View } from 'react-native';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Props, WebViewControl } from './types';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import useCss from './utils/useCss';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const dom = useMemo(() => {
|
||||
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
|
||||
// Use with caution -- don't load untrusted WebView HTML while testing.
|
||||
// Use with caution.
|
||||
return new JSDOM(props.html, { runScripts: 'dangerously', pretendToBeVisual: true });
|
||||
}, [props.html]);
|
||||
|
||||
const injectJs = useCallback((js: string) => {
|
||||
return dom.window.eval(js);
|
||||
}, [dom]);
|
||||
|
||||
useImperativeHandle(ref, (): WebViewControl => {
|
||||
const result = {
|
||||
injectJS: injectJs,
|
||||
injectJS(js: string) {
|
||||
return dom.window.eval(js);
|
||||
},
|
||||
postMessage(message: unknown) {
|
||||
const messageEventContent = {
|
||||
data: message,
|
||||
@@ -39,61 +36,33 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [dom, injectJs]);
|
||||
}, [dom]);
|
||||
|
||||
const onMessageRef = useRef(props.onMessage);
|
||||
onMessageRef.current = props.onMessage;
|
||||
|
||||
const { injectedJs: cssInjectedJavaScript } = useCss(
|
||||
injectJs,
|
||||
props.css,
|
||||
);
|
||||
|
||||
// Don't re-load when injected JS changes. This should match the behavior of the native webview.
|
||||
const injectedJavaScriptRef = useRef(props.injectedJavaScript);
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript + cssInjectedJavaScript;
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
// JSDOM polyfills
|
||||
dom.window.eval(`
|
||||
window.scrollBy = (_amount) => { };
|
||||
|
||||
// JSDOM iframes are missing certain functionality required by Joplin,
|
||||
// including:
|
||||
// - MessageEvent.source: Should point to the window that created a message.
|
||||
// Joplin uses this to determine the source of messages in iframe-related IPC.
|
||||
// - iframe.srcdoc: Used by Joplin to create plugin windows.
|
||||
const polyfillIframeContentWindow = (contentWindow) => {
|
||||
contentWindow.addEventListener('message', event => {
|
||||
// Work around a missing ".source" property on events.
|
||||
// See https://github.com/jsdom/jsdom/issues/2745#issuecomment-1207414024
|
||||
if (!event.source) {
|
||||
contentWindow.dispatchEvent(new MessageEvent('message', {
|
||||
source: window,
|
||||
data: event.data,
|
||||
}));
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
contentWindow.parent.postMessage = (message) => {
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
data: message,
|
||||
source: contentWindow,
|
||||
}));
|
||||
// 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 = () => {};
|
||||
range.getClientRects = () => {
|
||||
return {
|
||||
length: 0,
|
||||
item: () => null,
|
||||
[Symbol.iterator]: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
Object.defineProperty(HTMLIFrameElement.prototype, 'srcdoc', {
|
||||
set(value) {
|
||||
this.src = 'about:blank';
|
||||
setTimeout(() => {
|
||||
this.contentDocument.write(value);
|
||||
|
||||
polyfillIframeContentWindow(this.contentWindow);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
dom.window.eval(`
|
||||
@@ -108,14 +77,10 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Wrap the injected JavaScript in (() => {...})() to more closely
|
||||
// match the behavior of injectedJavaScript on Android -- variables
|
||||
// declared with "var" or "const" should not become global variables.
|
||||
dom.window.eval(`(() => {
|
||||
${injectedJavaScriptRef.current}
|
||||
})()`);
|
||||
dom.window.eval(injectedJavaScriptRef.current);
|
||||
}, [dom]);
|
||||
|
||||
|
||||
const onLoadEndRef = useRef(props.onLoadEnd);
|
||||
onLoadEndRef.current = props.onLoadEnd;
|
||||
const onLoadStartRef = useRef(props.onLoadStart);
|
||||
|
||||
@@ -12,7 +12,6 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Props, WebViewControl } from './types';
|
||||
import useCss from './utils/useCss';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
@@ -99,9 +98,6 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
}, 250);
|
||||
}, []);
|
||||
|
||||
const { injectedJs: cssInjectedJs } = useCss(webviewRef.current?.injectJavaScript, props.css);
|
||||
const injectedJavaScript = props.injectedJavaScript + cssInjectedJs;
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
|
||||
|
||||
@@ -135,7 +131,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
allowFileAccess={true}
|
||||
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
|
||||
webviewDebuggingEnabled={allowWebviewDebugging}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
injectedJavaScript={props.injectedJavaScript}
|
||||
onMessage={props.onMessage}
|
||||
onError={props.onError ?? onError}
|
||||
onLoadEnd={props.onLoadEnd}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useRef, useState,
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useRef, useState,
|
||||
} from 'react';
|
||||
import { Props, WebViewControl } from './types';
|
||||
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import makeSandboxedIframe from '@joplin/lib/utils/dom/makeSandboxedIframe';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useCss from './utils/useCss';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
@@ -21,26 +20,24 @@ const wrapperStyle: ViewStyle = { height: '100%', width: '100%', flex: 1 };
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement|null>(null);
|
||||
|
||||
const injectJs = useCallback((js: string) => {
|
||||
if (!iframeRef.current) {
|
||||
logger.warn(`WebView(${props.webviewInstanceId}): Tried to inject JavaScript after the iframe has unloaded.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// react-native-webview doesn't seem to show a warning in the case where JavaScript
|
||||
// is injected before the first page loads.
|
||||
if (!iframeRef.current.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
iframeRef.current.contentWindow.postMessage({
|
||||
injectJs: js,
|
||||
}, '*');
|
||||
}, [props.webviewInstanceId]);
|
||||
|
||||
useImperativeHandle(ref, (): WebViewControl => {
|
||||
return {
|
||||
injectJS: injectJs,
|
||||
injectJS(js: string) {
|
||||
if (!iframeRef.current) {
|
||||
logger.warn(`WebView(${props.webviewInstanceId}): Tried to inject JavaScript after the iframe has unloaded.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// react-native-webview doesn't seem to show a warning in the case where JavaScript
|
||||
// is injected before the first page loads.
|
||||
if (!iframeRef.current.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
iframeRef.current.contentWindow.postMessage({
|
||||
injectJs: js,
|
||||
}, '*');
|
||||
},
|
||||
postMessage(message: unknown) {
|
||||
if (!iframeRef.current || !iframeRef.current.contentWindow) {
|
||||
logger.warn(`WebView(${props.webviewInstanceId}): Tried to post a message to an unloaded iframe.`);
|
||||
@@ -52,7 +49,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
}, '*');
|
||||
},
|
||||
};
|
||||
}, [props.webviewInstanceId, injectJs]);
|
||||
}, [props.webviewInstanceId]);
|
||||
|
||||
const [containerElement, setContainerElement] = useState<HTMLDivElement>();
|
||||
const containerRef = useRef(containerElement);
|
||||
@@ -65,15 +62,9 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const onLoadStartRef = useRef(props.onLoadStart);
|
||||
onLoadStartRef.current = props.onLoadStart;
|
||||
|
||||
const { injectedJs: cssInjectedJs } = useCss(
|
||||
iframeRef.current ? injectJs : null,
|
||||
props.css,
|
||||
);
|
||||
const injectedJavaScript = props.injectedJavaScript + cssInjectedJs;
|
||||
|
||||
// Don't re-load when injected JS changes. This should match the behavior of the native webview.
|
||||
const injectedJavaScriptRef = useRef(injectedJavaScript);
|
||||
injectedJavaScriptRef.current = injectedJavaScript;
|
||||
const injectedJavaScriptRef = useRef(props.injectedJavaScript);
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
const headHtml = `
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface Props {
|
||||
|
||||
// If HTML is still being loaded, [html] should be an empty string.
|
||||
html: string;
|
||||
css?: string;
|
||||
|
||||
// Initial javascript. Must evaluate to true.
|
||||
injectedJavaScript: string;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type OnInjectJs = (js: string)=> void;
|
||||
|
||||
const webViewCssClassName = 'extended-webview-css';
|
||||
|
||||
const applyCssJs = (css: string) => `
|
||||
(function() {
|
||||
const styleId = ${JSON.stringify(webViewCssClassName)};
|
||||
|
||||
const oldStyle = document.getElementById(styleId);
|
||||
if (oldStyle) {
|
||||
oldStyle.remove();
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('id', styleId);
|
||||
|
||||
style.appendChild(document.createTextNode(${JSON.stringify(css)}));
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
|
||||
true;
|
||||
`;
|
||||
|
||||
const useCss = (injectJs: OnInjectJs|null, css: string) => {
|
||||
useEffect(() => {
|
||||
if (injectJs && css) {
|
||||
injectJs(applyCssJs(css));
|
||||
}
|
||||
}, [injectJs, css]);
|
||||
|
||||
return {
|
||||
injectedJs: css ? applyCssJs(css) : '',
|
||||
};
|
||||
};
|
||||
|
||||
export default useCss;
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent, ReactElement, useCallback, useContext } from 'react';
|
||||
import { FunctionComponent, ReactElement } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
import { themeStyle } from './global-style';
|
||||
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { DialogContext } from './DialogManager';
|
||||
|
||||
interface FolderPickerProps {
|
||||
disabled?: boolean;
|
||||
@@ -19,7 +16,6 @@ interface FolderPickerProps {
|
||||
darkText?: boolean;
|
||||
themeId?: number;
|
||||
coverableChildrenRight?: ReactElement|ReactElement[];
|
||||
onNewFolder?: (title: string)=> void;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +28,6 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
|
||||
placeholder,
|
||||
darkText,
|
||||
coverableChildrenRight,
|
||||
onNewFolder,
|
||||
themeId,
|
||||
}) => {
|
||||
const theme = themeStyle(themeId);
|
||||
@@ -66,15 +61,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
|
||||
return output;
|
||||
};
|
||||
|
||||
const dialogs = useContext(DialogContext);
|
||||
const onNewFolderPress = useCallback(async () => {
|
||||
const title = await dialogs.promptForText(_('New notebook title'));
|
||||
if (title !== null) {
|
||||
onNewFolder(title);
|
||||
}
|
||||
}, [dialogs, onNewFolder]);
|
||||
|
||||
const dropdown = (
|
||||
return (
|
||||
<Dropdown
|
||||
items={titlePickerItems(!!mustSelect)}
|
||||
accessibilityHint={_('Selects a notebook')}
|
||||
@@ -101,19 +88,6 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (onNewFolder) {
|
||||
return <View style={{ flexDirection: 'column', flex: 1 }}>
|
||||
{dropdown}
|
||||
<Button
|
||||
style={{ alignSelf: 'flex-end' }}
|
||||
icon='plus'
|
||||
onPress={onNewFolderPress}
|
||||
>{_('Create new notebook')}</Button>
|
||||
</View>;
|
||||
} else {
|
||||
return dropdown;
|
||||
}
|
||||
};
|
||||
|
||||
export default FolderPicker;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { TextStyle, Text, StyleProp } from 'react-native';
|
||||
import { TextStyle, Text } from 'react-native';
|
||||
|
||||
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
|
||||
const AntIcon = require('react-native-vector-icons/AntDesign').default;
|
||||
@@ -9,7 +9,7 @@ const Ionicon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
style: StyleProp<TextStyle>;
|
||||
style: TextStyle;
|
||||
|
||||
// If `null` is given, the content must be labeled elsewhere.
|
||||
accessibilityLabel: string|null;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import * as React from 'react';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useState, useMemo, useCallback, useRef, Ref } from 'react';
|
||||
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform, Role, StyleProp, View } from 'react-native';
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform, Role } from 'react-native';
|
||||
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
|
||||
import Icon from './Icon';
|
||||
import AccessibleView from './accessibility/AccessibleView';
|
||||
@@ -15,13 +15,11 @@ type ButtonClickListener = ()=> void;
|
||||
interface ButtonProps {
|
||||
onPress: ButtonClickListener;
|
||||
|
||||
pressableRef?: Ref<View>;
|
||||
|
||||
// Accessibility label and text shown in a tooltip
|
||||
description: string;
|
||||
|
||||
iconName: string;
|
||||
iconStyle: StyleProp<TextStyle>;
|
||||
iconStyle: TextStyle;
|
||||
|
||||
themeId: number;
|
||||
|
||||
@@ -89,7 +87,6 @@ const IconButton = (props: ButtonProps) => {
|
||||
|
||||
const button = (
|
||||
<Pressable
|
||||
ref={props.pressableRef}
|
||||
onPress={props.onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressIn={onPressIn}
|
||||
|
||||
@@ -1,88 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { ReactNode } from 'react';
|
||||
import { Text, View, StyleSheet, Button, TextStyle, ViewStyle } from 'react-native';
|
||||
import { themeStyle } from './global-style';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
import Modal from './Modal';
|
||||
import { PrimaryButton } from './buttons';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
children: React.ReactNode;
|
||||
ContentComponent: ReactNode;
|
||||
|
||||
buttonBarEnabled: boolean;
|
||||
okTitle: string;
|
||||
cancelTitle: string;
|
||||
title: string;
|
||||
onOkPress: ()=> void;
|
||||
onCancelPress: ()=> void;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
interface State {
|
||||
|
||||
}
|
||||
|
||||
class ModalDialog extends React.Component<Props, State> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private styles_: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
private styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 4,
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles: Record<string, ViewStyle|TextStyle> = {
|
||||
modalContentWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
maxWidth: 600,
|
||||
maxHeight: 500,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignSelf: 'center',
|
||||
marginVertical: 'auto',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
padding: theme.margin,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
margin: 20,
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
elevation: 10,
|
||||
},
|
||||
title: theme.headerStyle,
|
||||
contentWrapper: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
modalContentWrapper2: {
|
||||
flex: 1,
|
||||
},
|
||||
title: { ...theme.normalText, borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold' },
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.margin,
|
||||
marginTop: theme.marginTop,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.dividerColor,
|
||||
paddingTop: 10,
|
||||
},
|
||||
// Ensures that screen-reader-only headings have size (necessary for focusing/reading them).
|
||||
invisibleHeading: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
};
|
||||
|
||||
const ModalDialog: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
const theme = themeStyle(props.themeId);
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={true}
|
||||
onRequestClose={null}
|
||||
containerStyle={styles.container}
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
>
|
||||
<View style={styles.contentWrapper}>{props.children}</View>
|
||||
<View style={styles.buttonRow}>
|
||||
<View
|
||||
// This heading makes it easier for screen readers to jump to the
|
||||
// actions list. Without a heading here, it can be difficult to locate the "ok" and "cancel"
|
||||
// buttons.
|
||||
role='heading'
|
||||
aria-label={_('Actions')}
|
||||
accessible={true}
|
||||
style={styles.invisibleHeading}
|
||||
/>
|
||||
<Button disabled={!props.buttonBarEnabled} onPress={props.onCancelPress}>{props.cancelTitle}</Button>
|
||||
<PrimaryButton disabled={!props.buttonBarEnabled} onPress={props.onOkPress}>{props.okTitle}</PrimaryButton>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
public override render() {
|
||||
const ContentComponent = this.props.ContentComponent;
|
||||
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
|
||||
|
||||
return (
|
||||
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
|
||||
<Text style={this.styles().title}>{this.props.title}</Text>
|
||||
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
|
||||
<View style={this.styles().buttonRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 5 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalDialog;
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Ref, useRef, useImperativeHandle, useState, useCallback } from 'react';
|
||||
import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps, View, ViewProps } from 'react-native';
|
||||
|
||||
interface RenderEvent<T> {
|
||||
item: T;
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface ScrollToOptions {
|
||||
index: number;
|
||||
viewPosition: number;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
export interface NestableFlatListControl {
|
||||
scrollToIndex(options: ScrollToOptions): void;
|
||||
}
|
||||
|
||||
interface CellRendererProps<T> {
|
||||
index: number;
|
||||
item: T;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// For compatibility, these props should be mostly compatible with the
|
||||
// native FlatList props:
|
||||
interface Props<T> extends ScrollViewProps {
|
||||
ref: Ref<NestableFlatListControl>;
|
||||
data: T[];
|
||||
itemHeight: number;
|
||||
CellRendererComponent?: React.ComponentType<CellRendererProps<T>>;
|
||||
renderItem: (event: RenderEvent<T>)=> React.ReactNode;
|
||||
keyExtractor: (item: T)=> string;
|
||||
extraData: unknown;
|
||||
|
||||
// Additional props.
|
||||
// The contentWrapperProps can be used to improve accessibility by
|
||||
// applying certain content roles to the <View> that directly contains
|
||||
// the list's content. At least on web, applying these props directly to the ScrollView may
|
||||
// not work due to additional <View>s added by React Native.
|
||||
contentWrapperProps?: ViewProps;
|
||||
}
|
||||
|
||||
// This component allows working around restrictions on nesting React Native's built-in
|
||||
// <FlatList> within <ScrollView>s. For the most part, this component's interface should
|
||||
// be compatible with the <FlatList> API.
|
||||
//
|
||||
// See https://github.com/facebook/react-native/issues/31697.
|
||||
const NestableFlatList = function<T>({
|
||||
ref,
|
||||
itemHeight,
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
data,
|
||||
CellRendererComponent = React.Fragment,
|
||||
contentWrapperProps,
|
||||
...rest
|
||||
}: Props<T>) {
|
||||
const scrollViewRef = useRef<ScrollView|null>(null);
|
||||
const [scroll, setScroll] = useState(0);
|
||||
const [listHeight, setListHeight] = useState(0);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
scrollToIndex: ({ index, animated, viewPosition }) => {
|
||||
const offset = Math.max(0, index * itemHeight - viewPosition * listHeight);
|
||||
scrollViewRef.current.scrollTo({
|
||||
y: offset,
|
||||
animated,
|
||||
});
|
||||
// onScroll events don't seem to be sent when scrolling with .scrollTo.
|
||||
// The scroll offset needs to be updated manually:
|
||||
setScroll(offset);
|
||||
},
|
||||
};
|
||||
}, [itemHeight, listHeight]);
|
||||
|
||||
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
setScroll(event.nativeEvent.contentOffset.y);
|
||||
}, []);
|
||||
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
setListHeight(event.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const bufferSize = 10;
|
||||
const visibleStartIndex = Math.floor(scroll / itemHeight);
|
||||
const visibleEndIndex = Math.ceil((scroll + listHeight) / itemHeight);
|
||||
const startIndex = Math.max(0, visibleStartIndex - bufferSize);
|
||||
const maximumIndex = data.length - 1;
|
||||
const endIndex = Math.min(visibleEndIndex + bufferSize, maximumIndex);
|
||||
const paddingTop = startIndex * itemHeight;
|
||||
const paddingBottom = (maximumIndex - endIndex) * itemHeight;
|
||||
|
||||
const renderVisibleItems = () => {
|
||||
const result: React.ReactNode[] = [];
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
result.push(
|
||||
<CellRendererComponent
|
||||
index={i}
|
||||
item={data[i]}
|
||||
key={keyExtractor(data[i])}
|
||||
>
|
||||
{renderItem({ item: data[i], index: i })}
|
||||
</CellRendererComponent>,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return <ScrollView
|
||||
ref={scrollViewRef}
|
||||
onScroll={onScroll}
|
||||
onLayout={onLayout}
|
||||
{...rest}
|
||||
>
|
||||
<View {...contentWrapperProps}>
|
||||
<View style={{ height: paddingTop }}/>
|
||||
{renderVisibleItems()}
|
||||
<View style={{ height: paddingBottom }}/>
|
||||
</View>
|
||||
</ScrollView>;
|
||||
};
|
||||
|
||||
export default NestableFlatList;
|
||||
@@ -1,20 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
import { WebViewControl } from '../ExtendedWebView/types';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
import useRenderer from './hooks/useRenderer';
|
||||
import { OnWebViewMessageHandler } from './types';
|
||||
import useRerenderHandler, { ResourceInfo } from './hooks/useRerenderHandler';
|
||||
import useSource from './hooks/useSource';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import useContentScripts from './hooks/useContentScripts';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import useWebViewSetup from '../../contentScripts/rendererBundle/useWebViewSetup';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -65,14 +69,27 @@ function NoteBodyViewer(props: Props) {
|
||||
onResourceLongPress,
|
||||
});
|
||||
|
||||
const { api: renderer, pageSetup, webViewEventHandlers } = useWebViewSetup({
|
||||
const [webViewLoaded, setWebViewLoaded] = useState(false);
|
||||
const [onWebViewMessage, setOnWebViewMessage] = useState<OnWebViewMessageHandler>(()=>()=>{});
|
||||
|
||||
|
||||
// The renderer can write to whichever temporary directory we choose. As such,
|
||||
// we use a subdirectory of the main temporary directory for security reasons.
|
||||
const tempDir = useMemo(() => {
|
||||
return `${Setting.value('tempDir')}/${uuid.createNano()}`;
|
||||
}, []);
|
||||
|
||||
const renderer = useRenderer({
|
||||
webViewLoaded,
|
||||
onScroll,
|
||||
webviewRef,
|
||||
onBodyScroll: onScroll,
|
||||
onPostMessage,
|
||||
pluginStates: props.pluginStates,
|
||||
themeId: props.themeId,
|
||||
setOnWebViewMessage,
|
||||
tempDir,
|
||||
});
|
||||
|
||||
const contentScripts = useContentScripts(props.pluginStates);
|
||||
|
||||
useRerenderHandler({
|
||||
renderer,
|
||||
fontSize: props.fontSize,
|
||||
@@ -85,14 +102,16 @@ function NoteBodyViewer(props: Props) {
|
||||
initialScroll: props.initialScroll,
|
||||
|
||||
paddingBottom: props.paddingBottom,
|
||||
|
||||
contentScripts,
|
||||
});
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
webViewEventHandlers.onLoadEnd();
|
||||
setWebViewLoaded(true);
|
||||
if (props.onLoadEnd) props.onLoadEnd();
|
||||
}, [props.onLoadEnd, webViewEventHandlers]);
|
||||
}, [props.onLoadEnd]);
|
||||
|
||||
const { html, js } = useSource(pageSetup, props.themeId);
|
||||
const { html, injectedJs } = useSource(tempDir, props.themeId);
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
@@ -102,10 +121,10 @@ function NoteBodyViewer(props: Props) {
|
||||
testID='NoteBodyViewer'
|
||||
html={html}
|
||||
allowFileAccessFromJs={true}
|
||||
injectedJavaScript={js}
|
||||
injectedJavaScript={injectedJs}
|
||||
mixedContentMode="always"
|
||||
onLoadEnd={onLoadEnd}
|
||||
onMessage={webViewEventHandlers.onMessage}
|
||||
onMessage={onWebViewMessage}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
/** @jest-environment jsdom */
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Renderer, { RenderSettings, RendererSetupOptions } from './Renderer';
|
||||
import Renderer, { RendererSettings, RendererSetupOptions } from './Renderer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
|
||||
const defaultRendererSettings: RenderSettings = {
|
||||
const defaultRendererSettings: RendererSettings = {
|
||||
theme: JSON.stringify({ cacheKey: 'test' }),
|
||||
onResourceLoaded: ()=>{},
|
||||
highlightedKeywords: [],
|
||||
resources: {},
|
||||
codeTheme: 'atom-one-light.css',
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
readAssetBlob: async (_path: string) => new Blob(),
|
||||
removeUnusedPluginAssets: true,
|
||||
readAssetBlob: async (_path: string)=>new Blob(),
|
||||
|
||||
createEditPopupSyntax: '',
|
||||
destroyEditPopupSyntax: '',
|
||||
pluginAssetContainerSelector: '#asset-container',
|
||||
splitted: false,
|
||||
|
||||
pluginSettings: {},
|
||||
requestPluginSetting: () => { },
|
||||
requestPluginSetting: ()=>{},
|
||||
};
|
||||
|
||||
const makeRenderer = (options: Partial<RendererSetupOptions>) => {
|
||||
@@ -49,25 +47,25 @@ describe('Renderer', () => {
|
||||
document.body.appendChild(contentContainer);
|
||||
|
||||
const pluginAssetsContainer = document.createElement('div');
|
||||
pluginAssetsContainer.id = 'asset-container';
|
||||
pluginAssetsContainer.id = 'joplin-container-pluginAssetsContainer';
|
||||
document.body.appendChild(pluginAssetsContainer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelector('#joplin-container-content')?.remove();
|
||||
document.querySelector('#asset-container')?.remove();
|
||||
document.querySelector('#joplin-container-pluginAssetsContainer')?.remove();
|
||||
});
|
||||
|
||||
test('should support rendering markdown', async () => {
|
||||
const renderer = makeRenderer({});
|
||||
await renderer.rerenderToBody(
|
||||
await renderer.rerender(
|
||||
{ language: MarkupLanguage.Markdown, markup: '**test**' },
|
||||
defaultRendererSettings,
|
||||
);
|
||||
|
||||
expect(getRenderedContent().innerHTML.trim()).toBe('<p><strong>test</strong></p>');
|
||||
|
||||
await renderer.rerenderToBody(
|
||||
await renderer.rerender(
|
||||
{ language: MarkupLanguage.Markdown, markup: '*test*' },
|
||||
defaultRendererSettings,
|
||||
);
|
||||
@@ -94,7 +92,7 @@ describe('Renderer', () => {
|
||||
pluginId: 'com.example.test-plugin',
|
||||
},
|
||||
]);
|
||||
await renderer.rerenderToBody(
|
||||
await renderer.rerender(
|
||||
{ language: MarkupLanguage.Markdown, markup: '```\ntest\n```' },
|
||||
defaultRendererSettings,
|
||||
);
|
||||
@@ -102,7 +100,7 @@ describe('Renderer', () => {
|
||||
|
||||
// Should support removing plugin scripts
|
||||
await renderer.setExtraContentScriptsAndRerender([]);
|
||||
await renderer.rerenderToBody(
|
||||
await renderer.rerender(
|
||||
{ language: MarkupLanguage.Markdown, markup: '```\ntest\n```' },
|
||||
defaultRendererSettings,
|
||||
);
|
||||
@@ -115,14 +113,14 @@ describe('Renderer', () => {
|
||||
|
||||
const requestPluginSetting = jest.fn();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const rerenderToBody = (pluginSettings: Record<string, any>) => {
|
||||
return renderer.rerenderToBody(
|
||||
const rerender = (pluginSettings: Record<string, any>) => {
|
||||
return renderer.rerender(
|
||||
{ language: MarkupLanguage.Markdown, markup: '```\ntest\n```' },
|
||||
{ ...defaultRendererSettings, pluginSettings, requestPluginSetting },
|
||||
);
|
||||
};
|
||||
|
||||
await rerenderToBody({});
|
||||
await rerender({});
|
||||
expect(requestPluginSetting).toHaveBeenCalledTimes(0);
|
||||
|
||||
const pluginId = 'com.example.test-plugin';
|
||||
@@ -148,7 +146,7 @@ describe('Renderer', () => {
|
||||
|
||||
// Should call .requestPluginSetting for missing settings
|
||||
expect(requestPluginSetting).toHaveBeenCalledTimes(1);
|
||||
await rerenderToBody({ someOtherSetting: 1 });
|
||||
await rerender({});
|
||||
expect(requestPluginSetting).toHaveBeenCalledTimes(2);
|
||||
expect(requestPluginSetting).toHaveBeenLastCalledWith('com.example.test-plugin', 'setting');
|
||||
|
||||
@@ -156,11 +154,11 @@ describe('Renderer', () => {
|
||||
expect(getRenderedContent().querySelector('#setting-value').innerHTML).toBe('Setting value: undefined');
|
||||
|
||||
// Should expect only namespaced plugin settings
|
||||
await rerenderToBody({ 'setting': 'test' });
|
||||
await rerender({ 'setting': 'test' });
|
||||
expect(requestPluginSetting).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Should not request plugin settings when all settings are present.
|
||||
await rerenderToBody({ [`${pluginId}.setting`]: 'test' });
|
||||
await rerender({ [`${pluginId}.setting`]: 'test' });
|
||||
expect(requestPluginSetting).toHaveBeenCalledTimes(3);
|
||||
expect(getRenderedContent().querySelector('#setting-value').innerHTML).toBe('Setting value: test');
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import type { MarkupToHtmlConverter, RenderOptions, RenderResultPluginAsset, FsDriver as RendererFsDriver } from '@joplin/renderer/types';
|
||||
import makeResourceModel from './utils/makeResourceModel';
|
||||
import addPluginAssets from './utils/addPluginAssets';
|
||||
import { ExtraContentScriptSource } from './types';
|
||||
import { ExtraContentScript } from '@joplin/lib/services/plugins/utils/loadContentScripts';
|
||||
|
||||
export interface RendererSetupOptions {
|
||||
settings: {
|
||||
safeMode: boolean;
|
||||
tempDir: string;
|
||||
resourceDir: string;
|
||||
resourceDownloadMode: string;
|
||||
};
|
||||
// True if asset and resource files should be transferred to the WebView before rendering.
|
||||
// This must be true on web, where asset and resource files are virtual and can't be accessed
|
||||
// without transferring.
|
||||
useTransferredFiles: boolean;
|
||||
|
||||
fsDriver: RendererFsDriver;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
pluginOptions: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface RendererSettings {
|
||||
theme: string;
|
||||
onResourceLoaded: ()=> void;
|
||||
highlightedKeywords: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
resources: Record<string, any>;
|
||||
codeTheme: string;
|
||||
noteHash: string;
|
||||
initialScroll: number;
|
||||
|
||||
createEditPopupSyntax: string;
|
||||
destroyEditPopupSyntax: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
pluginSettings: Record<string, any>;
|
||||
requestPluginSetting: (pluginId: string, settingKey: string)=> void;
|
||||
readAssetBlob: (assetPath: string)=> Promise<Blob>;
|
||||
}
|
||||
|
||||
export interface MarkupRecord {
|
||||
language: MarkupLanguage;
|
||||
markup: string;
|
||||
}
|
||||
|
||||
export default class Renderer {
|
||||
private markupToHtml: MarkupToHtmlConverter;
|
||||
private lastSettings: RendererSettings|null = null;
|
||||
private extraContentScripts: ExtraContentScript[] = [];
|
||||
private lastRenderMarkup: MarkupRecord|null = null;
|
||||
private resourcePathOverrides: Record<string, string> = Object.create(null);
|
||||
|
||||
public constructor(private setupOptions: RendererSetupOptions) {
|
||||
this.recreateMarkupToHtml();
|
||||
}
|
||||
|
||||
private recreateMarkupToHtml() {
|
||||
this.markupToHtml = new MarkupToHtml({
|
||||
extraRendererRules: this.extraContentScripts,
|
||||
fsDriver: this.setupOptions.fsDriver,
|
||||
isSafeMode: this.setupOptions.settings.safeMode,
|
||||
tempDir: this.setupOptions.settings.tempDir,
|
||||
ResourceModel: makeResourceModel(this.setupOptions.settings.resourceDir),
|
||||
pluginOptions: this.setupOptions.pluginOptions,
|
||||
});
|
||||
}
|
||||
|
||||
// Intended for web, where resources can't be linked to normally.
|
||||
public async setResourceFile(id: string, file: Blob) {
|
||||
this.resourcePathOverrides[id] = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
public getResourcePathOverride(resourceId: string) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.resourcePathOverrides, resourceId)) {
|
||||
return this.resourcePathOverrides[resourceId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async setExtraContentScriptsAndRerender(
|
||||
extraContentScripts: ExtraContentScriptSource[],
|
||||
) {
|
||||
this.extraContentScripts = extraContentScripts.map(script => {
|
||||
const scriptModule = (eval(script.js))({
|
||||
pluginId: script.pluginId,
|
||||
contentScriptId: script.id,
|
||||
});
|
||||
|
||||
if (!scriptModule.plugin) {
|
||||
throw new Error(`
|
||||
Expected content script ${script.id} to export a function that returns an object with a "plugin" property.
|
||||
Found: ${scriptModule}, which has keys ${Object.keys(scriptModule)}.
|
||||
`);
|
||||
}
|
||||
|
||||
return {
|
||||
...script,
|
||||
module: scriptModule,
|
||||
};
|
||||
});
|
||||
this.recreateMarkupToHtml();
|
||||
|
||||
// If possible, rerenders with the last rendering settings. The goal
|
||||
// of this is to reduce the number of IPC calls between the viewer and
|
||||
// React Native. We want the first render to be as fast as possible.
|
||||
if (this.lastRenderMarkup) {
|
||||
await this.rerender(this.lastRenderMarkup, this.lastSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public async rerender(markup: MarkupRecord, settings: RendererSettings) {
|
||||
this.lastSettings = settings;
|
||||
this.lastRenderMarkup = markup;
|
||||
|
||||
const options: RenderOptions = {
|
||||
onResourceLoaded: settings.onResourceLoaded,
|
||||
highlightedKeywords: settings.highlightedKeywords,
|
||||
resources: settings.resources,
|
||||
codeTheme: settings.codeTheme,
|
||||
postMessageSyntax: 'window.joplinPostMessage_',
|
||||
enableLongPress: true,
|
||||
|
||||
// Show an 'edit' popup over SVG images
|
||||
editPopupFiletypes: ['image/svg+xml'],
|
||||
createEditPopupSyntax: settings.createEditPopupSyntax,
|
||||
destroyEditPopupSyntax: settings.destroyEditPopupSyntax,
|
||||
itemIdToUrl: this.setupOptions.useTransferredFiles ? (id: string) => this.getResourcePathOverride(id) : undefined,
|
||||
|
||||
settingValue: (pluginId: string, settingName: string) => {
|
||||
const settingKey = `${pluginId}.${settingName}`;
|
||||
|
||||
if (!(settingKey in settings.pluginSettings)) {
|
||||
// This should make the setting available on future renders.
|
||||
settings.requestPluginSetting(pluginId, settingName);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return settings.pluginSettings[settingKey];
|
||||
},
|
||||
whiteBackgroundNoteRendering: markup.language === MarkupLanguage.Html,
|
||||
};
|
||||
|
||||
this.markupToHtml.clearCache(markup.language);
|
||||
|
||||
const contentContainer = document.getElementById('joplin-container-content');
|
||||
|
||||
let html = '';
|
||||
let pluginAssets: RenderResultPluginAsset[] = [];
|
||||
try {
|
||||
const result = await this.markupToHtml.render(
|
||||
markup.language,
|
||||
markup.markup,
|
||||
JSON.parse(settings.theme),
|
||||
options,
|
||||
);
|
||||
html = result.html;
|
||||
pluginAssets = result.pluginAssets;
|
||||
} catch (error) {
|
||||
if (!contentContainer) {
|
||||
alert(`Renderer error: ${error}`);
|
||||
} else {
|
||||
contentContainer.innerText = `
|
||||
Error: ${error}
|
||||
|
||||
${error.stack ?? ''}
|
||||
`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
contentContainer.innerHTML = html;
|
||||
|
||||
// Adding plugin assets can be slow -- run it asynchronously.
|
||||
void (async () => {
|
||||
await addPluginAssets(pluginAssets, {
|
||||
inlineAssets: this.setupOptions.useTransferredFiles,
|
||||
readAssetBlob: settings.readAssetBlob,
|
||||
});
|
||||
|
||||
// Some plugins require this event to be dispatched just after being added.
|
||||
document.dispatchEvent(new Event('joplin-noteDidUpdate'));
|
||||
})();
|
||||
|
||||
this.afterRender(settings);
|
||||
}
|
||||
|
||||
private afterRender(renderSettings: RendererSettings) {
|
||||
const readyStateCheckInterval = setInterval(() => {
|
||||
if (document.readyState === 'complete') {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
if (this.setupOptions.settings.resourceDownloadMode === 'manual') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(window as any).webviewLib.setupResourceManualDownload();
|
||||
}
|
||||
|
||||
const hash = renderSettings.noteHash;
|
||||
const initialScroll = renderSettings.initialScroll;
|
||||
|
||||
// Don't scroll to a hash if we're given initial scroll (initial scroll
|
||||
// overrides scrolling to a hash).
|
||||
if ((initialScroll ?? null) !== null) {
|
||||
const scrollingElement = document.scrollingElement ?? document.documentElement;
|
||||
scrollingElement.scrollTop = initialScroll;
|
||||
} else if (hash) {
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
setTimeout(() => {
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
console.warn('Cannot find hash', hash);
|
||||
return;
|
||||
}
|
||||
e.scrollIntoView();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
public clearCache(markupLanguage: MarkupLanguage) {
|
||||
this.markupToHtml.clearCache(markupLanguage);
|
||||
}
|
||||
|
||||
private extraCssElements: Record<string, HTMLStyleElement> = {};
|
||||
public setExtraCss(key: string, css: string) {
|
||||
if (this.extraCssElements.hasOwnProperty(key)) {
|
||||
this.extraCssElements[key].remove();
|
||||
}
|
||||
|
||||
const extraCssElement = document.createElement('style');
|
||||
extraCssElement.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(extraCssElement);
|
||||
|
||||
this.extraCssElements[key] = extraCssElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger';
|
||||
import { NoteViewerLocalApi, NoteViewerRemoteApi, RendererWebViewOptions, WebViewLib } from './types';
|
||||
import Renderer from './Renderer';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
rendererWebViewOptions: RendererWebViewOptions;
|
||||
webviewLib: WebViewLib;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
declare const webviewLib: WebViewLib;
|
||||
|
||||
const messenger = new WebViewToRNMessenger<NoteViewerLocalApi, NoteViewerRemoteApi>(
|
||||
'note-viewer',
|
||||
null,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(window as any).joplinPostMessage_ = (message: string, _args: any) => {
|
||||
return messenger.remoteApi.onPostMessage(message);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(window as any).webviewApi = {
|
||||
postMessage: messenger.remoteApi.onPostPluginMessage,
|
||||
};
|
||||
|
||||
webviewLib.initialize({
|
||||
postMessage: (message: string) => {
|
||||
messenger.remoteApi.onPostMessage(message);
|
||||
},
|
||||
});
|
||||
// Share the webview library globally so that the renderer can access it.
|
||||
window.webviewLib = webviewLib;
|
||||
|
||||
window.webviewLib = webviewLib;
|
||||
|
||||
const renderer = new Renderer({
|
||||
...window.rendererWebViewOptions,
|
||||
fsDriver: messenger.remoteApi.fsDriver,
|
||||
});
|
||||
|
||||
messenger.setLocalInterface({
|
||||
renderer,
|
||||
jumpToHash: (hash: string) => {
|
||||
location.hash = `#${hash}`;
|
||||
},
|
||||
});
|
||||
|
||||
const lastScrollTop: number|null = null;
|
||||
const onMainContentScroll = () => {
|
||||
const newScrollTop = document.scrollingElement.scrollTop;
|
||||
if (lastScrollTop !== newScrollTop) {
|
||||
messenger.remoteApi.onScroll(newScrollTop);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for events on both scrollingElement and window
|
||||
// - On Android, scrollingElement.addEventListener('scroll', callback) doesn't call callback on
|
||||
// scroll. However, window.addEventListener('scroll', callback) does.
|
||||
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
|
||||
// the listener is added to window with window.addEventListener('scroll', ...).
|
||||
document.scrollingElement?.addEventListener('scroll', onMainContentScroll);
|
||||
window.addEventListener('scroll', onMainContentScroll);
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FsDriver as RendererFsDriver } from '@joplin/renderer/types';
|
||||
import Renderer from './Renderer';
|
||||
|
||||
export interface RendererWebViewOptions {
|
||||
settings: {
|
||||
safeMode: boolean;
|
||||
tempDir: string;
|
||||
resourceDir: string;
|
||||
resourceDownloadMode: string;
|
||||
};
|
||||
useTransferredFiles: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
pluginOptions: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ExtraContentScriptSource {
|
||||
id: string;
|
||||
js: string;
|
||||
assetPath: string;
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface NoteViewerLocalApi {
|
||||
renderer: Renderer;
|
||||
jumpToHash: (hash: string)=> void;
|
||||
}
|
||||
|
||||
export interface NoteViewerRemoteApi {
|
||||
onScroll(scrollTop: number): void;
|
||||
onPostMessage(message: string): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onPostPluginMessage(contentScriptId: string, message: any): Promise<any>;
|
||||
fsDriver: RendererFsDriver;
|
||||
}
|
||||
|
||||
export interface WebViewLib {
|
||||
initialize(config: unknown): void;
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ const rewriteInternalAssetLinks = async (asset: RenderResultPluginAsset, content
|
||||
|
||||
interface Options {
|
||||
inlineAssets: boolean;
|
||||
removeUnusedPluginAssets: boolean;
|
||||
container: HTMLElement;
|
||||
readAssetBlob?(path: string): Promise<Blob>;
|
||||
}
|
||||
|
||||
@@ -49,7 +47,7 @@ interface Options {
|
||||
const addPluginAssets = async (assets: RenderResultPluginAsset[], options: Options) => {
|
||||
if (!assets) return;
|
||||
|
||||
const pluginAssetsContainer = options.container;
|
||||
const pluginAssetsContainer = document.getElementById('joplin-container-pluginAssetsContainer');
|
||||
|
||||
const prepareAssetBlobUrls = () => {
|
||||
for (const asset of assets) {
|
||||
@@ -138,22 +136,16 @@ const addPluginAssets = async (assets: RenderResultPluginAsset[], options: Optio
|
||||
// light to dark theme, and then back to light theme - in that case
|
||||
// the viewer would remain dark because it would use the dark
|
||||
// stylesheet that would still be in the DOM.
|
||||
//
|
||||
// In some cases, however, we only want to rerender part of the document.
|
||||
// In this case, old plugin assets may have been from the last full-page
|
||||
// render and should not be removed.
|
||||
if (options.removeUnusedPluginAssets) {
|
||||
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
|
||||
if (!processedAssetIds.includes(assetId)) {
|
||||
try {
|
||||
asset.element.remove();
|
||||
} catch (error) {
|
||||
// We don't throw an exception but we log it since
|
||||
// it shouldn't happen
|
||||
console.warn('Tried to remove an asset but got an error', error);
|
||||
}
|
||||
pluginAssetsAdded_[assetId] = null;
|
||||
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
|
||||
if (!processedAssetIds.includes(assetId)) {
|
||||
try {
|
||||
asset.element.remove();
|
||||
} catch (error) {
|
||||
// We don't throw an exception but we log it since
|
||||
// it shouldn't happen
|
||||
console.warn('Tried to remove an asset but got an error', error);
|
||||
}
|
||||
pluginAssetsAdded_[assetId] = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { useRef, useState } from 'react';
|
||||
import { ExtraContentScriptSource } from '../bundledJs/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ExtraContentScriptSource } from '../types';
|
||||
|
||||
const logger = Logger.create('NoteBodyViewer/hooks/useContentScripts');
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Dispatch, RefObject, SetStateAction, useEffect, useMemo, useRef } from 'react';
|
||||
import { WebViewControl } from '../../ExtendedWebView/types';
|
||||
import { OnScrollCallback, OnWebViewMessageHandler } from '../types';
|
||||
import RNToWebViewMessenger from '../../../utils/ipc/RNToWebViewMessenger';
|
||||
import { NoteViewerLocalApi, NoteViewerRemoteApi } from '../bundledJs/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('useRenderer');
|
||||
|
||||
interface Props {
|
||||
webviewRef: RefObject<WebViewControl>;
|
||||
onScroll: OnScrollCallback;
|
||||
onPostMessage: (message: string)=> void;
|
||||
setOnWebViewMessage: Dispatch<SetStateAction<OnWebViewMessageHandler>>;
|
||||
webViewLoaded: boolean;
|
||||
|
||||
tempDir: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onPostPluginMessage = async (contentScriptId: string, message: any) => {
|
||||
logger.debug(`Handling message from content script: ${contentScriptId}:`, message);
|
||||
|
||||
const pluginService = PluginService.instance();
|
||||
const pluginId = pluginService.pluginIdByContentScriptId(contentScriptId);
|
||||
if (!pluginId) {
|
||||
throw new Error(`Plugin not found for content script with ID ${contentScriptId}`);
|
||||
}
|
||||
|
||||
const plugin = pluginService.pluginById(pluginId);
|
||||
return plugin.emitContentScriptMessage(contentScriptId, message);
|
||||
};
|
||||
|
||||
const useRenderer = (props: Props) => {
|
||||
const onScrollRef = useRef(props.onScroll);
|
||||
onScrollRef.current = props.onScroll;
|
||||
|
||||
const onPostMessageRef = useRef(props.onPostMessage);
|
||||
onPostMessageRef.current = props.onPostMessage;
|
||||
|
||||
const messenger = useMemo(() => {
|
||||
const fsDriver = shim.fsDriver();
|
||||
const localApi = {
|
||||
onScroll: (fraction: number) => onScrollRef.current?.(fraction),
|
||||
onPostMessage: (message: string) => onPostMessageRef.current?.(message),
|
||||
onPostPluginMessage,
|
||||
fsDriver: {
|
||||
writeFile: async (path: string, content: string, encoding?: string) => {
|
||||
if (!await fsDriver.exists(props.tempDir)) {
|
||||
await fsDriver.mkdir(props.tempDir);
|
||||
}
|
||||
// To avoid giving the WebView access to the entire main tempDir,
|
||||
// we use props.tempDir (which should be different).
|
||||
path = fsDriver.resolveRelativePathWithinDir(props.tempDir, path);
|
||||
return await fsDriver.writeFile(path, content, encoding);
|
||||
},
|
||||
exists: fsDriver.exists,
|
||||
cacheCssToFile: fsDriver.cacheCssToFile,
|
||||
},
|
||||
};
|
||||
return new RNToWebViewMessenger<NoteViewerRemoteApi, NoteViewerLocalApi>(
|
||||
'note-viewer', props.webviewRef, localApi,
|
||||
);
|
||||
}, [props.webviewRef, props.tempDir]);
|
||||
|
||||
useEffect(() => {
|
||||
props.setOnWebViewMessage(() => (event: WebViewMessageEvent) => {
|
||||
messenger.onWebViewMessage(event);
|
||||
});
|
||||
}, [messenger, props.setOnWebViewMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.webViewLoaded) {
|
||||
messenger.onWebViewLoaded();
|
||||
}
|
||||
}, [messenger, props.webViewLoaded]);
|
||||
|
||||
return useMemo(() => {
|
||||
return messenger.remoteApi.renderer;
|
||||
}, [messenger]);
|
||||
};
|
||||
|
||||
export default useRenderer;
|
||||
@@ -1,20 +1,26 @@
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
import { RendererControl, RenderOptions } from '../../../contentScripts/rendererBundle/types';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import useEditPopup from './useEditPopup';
|
||||
import Renderer from '../bundledJs/Renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { ExtraContentScriptSource } from '../bundledJs/types';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
|
||||
|
||||
|
||||
export interface ResourceInfo {
|
||||
localState: ResourceLocalStateEntity;
|
||||
localState: unknown;
|
||||
item: ResourceEntity;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
renderer: RendererControl;
|
||||
renderer: Renderer;
|
||||
|
||||
noteBody: string;
|
||||
noteMarkupLanguage: MarkupLanguage;
|
||||
@@ -27,6 +33,8 @@ interface Props {
|
||||
initialScroll: number|undefined;
|
||||
|
||||
paddingBottom: number;
|
||||
|
||||
contentScripts: ExtraContentScriptSource[];
|
||||
}
|
||||
|
||||
const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
@@ -48,35 +56,10 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
|
||||
const logger = Logger.create('useRerenderHandler');
|
||||
|
||||
const useResourceLoadCounter = (noteResources: Record<string, ResourceInfo>) => {
|
||||
const [lastResourceLoadCounter, setLastResourceLoadCounter] = useState(0);
|
||||
const lastDownloadCount = useRef(-1);
|
||||
useEffect(() => {
|
||||
let downloadedCount = 0;
|
||||
for (const resource of Object.values(noteResources)) {
|
||||
if (resource.localState.fetch_status === Resource.FETCH_STATUS_DONE) {
|
||||
downloadedCount ++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastDownloadCount.current !== -1 && lastDownloadCount.current < downloadedCount) {
|
||||
setLastResourceLoadCounter(counter => counter + 1);
|
||||
}
|
||||
lastDownloadCount.current = downloadedCount;
|
||||
}, [noteResources]);
|
||||
|
||||
return lastResourceLoadCounter;
|
||||
};
|
||||
|
||||
const useRerenderHandler = (props: Props) => {
|
||||
const resourceDownloadRerenderCounter = useResourceLoadCounter(props.noteResources);
|
||||
useEffect(() => {
|
||||
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
|
||||
// props changes, thus triggering a render. The **content** of this noteResources array however is not changed because
|
||||
// it doesn't contain info about the resource download state. Because of that, if we were to use the markupToHtml() cache
|
||||
// it wouldn't re-render at all.
|
||||
props.renderer.clearCache(props.noteMarkupLanguage);
|
||||
}, [resourceDownloadRerenderCounter, props.renderer, props.noteMarkupLanguage]);
|
||||
const { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss } = useEditPopup(props.themeId);
|
||||
const [lastResourceLoadCounter, setLastResourceLoadCounter] = useState(0);
|
||||
const [pluginSettingKeys, setPluginSettingKeys] = useState<Record<string, boolean>>({});
|
||||
|
||||
// To address https://github.com/laurent22/joplin/issues/433
|
||||
//
|
||||
@@ -99,8 +82,8 @@ const useRerenderHandler = (props: Props) => {
|
||||
// below logic rely on this.
|
||||
const effectDependencies = [
|
||||
props.noteBody, props.noteMarkupLanguage, props.renderer, props.highlightedKeywords,
|
||||
props.noteHash, props.noteResources, props.themeId, props.paddingBottom, resourceDownloadRerenderCounter,
|
||||
props.fontSize,
|
||||
props.noteHash, props.noteResources, props.themeId, props.paddingBottom, lastResourceLoadCounter,
|
||||
createEditPopupSyntax, destroyEditPopupSyntax, pluginSettingKeys, props.fontSize,
|
||||
];
|
||||
const previousDeps = usePrevious(effectDependencies, []);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -116,43 +99,125 @@ const useRerenderHandler = (props: Props) => {
|
||||
const previousHash = usePrevious(props.noteHash, '');
|
||||
const hashChanged = previousHash !== props.noteHash;
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
useEffect(() => {
|
||||
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
|
||||
// props changes, thus triggering a render. The **content** of this noteResources array however is not changed because
|
||||
// it doesn't contain info about the resource download state. Because of that, if we were to use the markupToHtml() cache
|
||||
// it wouldn't re-render at all.
|
||||
props.renderer.clearCache(props.noteMarkupLanguage);
|
||||
}, [lastResourceLoadCounter, props.renderer, props.noteMarkupLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
void props.renderer.setExtraContentScriptsAndRerender(props.contentScripts);
|
||||
}, [props.contentScripts, props.renderer]);
|
||||
|
||||
useAsyncEffect(async event => {
|
||||
if (onlyNoteBodyHasChanged && onlyCheckboxesHaveChanged) {
|
||||
logger.info('Only a checkbox has changed - not updating HTML');
|
||||
return;
|
||||
}
|
||||
|
||||
const config: RenderOptions = {
|
||||
themeId: props.themeId,
|
||||
themeOverrides: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const pluginSettings: Record<string, any> = { };
|
||||
for (const key in pluginSettingKeys) {
|
||||
pluginSettings[key] = Setting.value(`plugin-${key}`);
|
||||
}
|
||||
let newPluginSettingKeys = pluginSettingKeys;
|
||||
|
||||
// On web, resources are virtual files and thus need to be transferred to the WebView.
|
||||
if (shim.mobilePlatform() === 'web') {
|
||||
for (const [resourceId, resource] of Object.entries(props.noteResources)) {
|
||||
try {
|
||||
await props.renderer.setResourceFile(
|
||||
resourceId,
|
||||
await shim.fsDriver().fileAtPath(Resource.fullPath(resource.item)),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// This can happen if a resource hasn't been downloaded yet
|
||||
logger.warn('Error: Resource file not found (ENOENT)', Resource.fullPath(resource.item), 'for ID', resource.item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const config = {
|
||||
// We .stringify the theme to avoid a JSON serialization error involving
|
||||
// the color package.
|
||||
theme: JSON.stringify({
|
||||
bodyPaddingTop: '0.8em',
|
||||
bodyPaddingBottom: props.paddingBottom,
|
||||
...theme,
|
||||
|
||||
noteViewerFontSize: props.fontSize,
|
||||
}),
|
||||
codeTheme: theme.codeThemeCss,
|
||||
|
||||
onResourceLoaded: () => {
|
||||
// Force a rerender when a resource loads
|
||||
setLastResourceLoadCounter(lastResourceLoadCounter + 1);
|
||||
},
|
||||
highlightedKeywords: props.highlightedKeywords,
|
||||
resources: props.noteResources,
|
||||
pluginAssetContainerSelector: '#joplin-container-pluginAssetsContainer',
|
||||
removeUnusedPluginAssets: true,
|
||||
|
||||
// If the hash changed, we don't set initial scroll -- we want to scroll to the hash
|
||||
// instead.
|
||||
initialScroll: (previousHash && hashChanged) ? undefined : props.initialScroll,
|
||||
noteHash: props.noteHash,
|
||||
|
||||
pluginSettings,
|
||||
requestPluginSetting: (pluginId: string, settingKey: string) => {
|
||||
// Don't trigger additional renders
|
||||
if (event.cancelled) return;
|
||||
|
||||
const key = `${pluginId}.${settingKey}`;
|
||||
logger.debug(`Request plugin setting: plugin-${key}`);
|
||||
|
||||
if (!(key in newPluginSettingKeys)) {
|
||||
newPluginSettingKeys = { ...newPluginSettingKeys, [`${pluginId}.${settingKey}`]: true };
|
||||
setPluginSettingKeys(newPluginSettingKeys);
|
||||
}
|
||||
},
|
||||
readAssetBlob: (assetPath: string) => {
|
||||
// Built-in assets are in resourceDir, external plugin assets are in cacheDir.
|
||||
const assetsDirs = [Setting.value('resourceDir'), Setting.value('cacheDir')];
|
||||
|
||||
let resolvedPath = null;
|
||||
for (const assetDir of assetsDirs) {
|
||||
resolvedPath ??= resolvePathWithinDir(assetDir, assetPath);
|
||||
if (resolvedPath) break;
|
||||
}
|
||||
|
||||
if (!resolvedPath) {
|
||||
throw new Error(`Failed to load asset at ${assetPath} -- not in any of the allowed asset directories: ${assetsDirs.join(',')}.`);
|
||||
}
|
||||
return shim.fsDriver().fileAtPath(resolvedPath);
|
||||
},
|
||||
|
||||
createEditPopupSyntax,
|
||||
destroyEditPopupSyntax,
|
||||
};
|
||||
|
||||
try {
|
||||
logger.debug('Starting render...');
|
||||
|
||||
await props.renderer.rerenderToBody({
|
||||
await props.renderer.rerender({
|
||||
language: props.noteMarkupLanguage,
|
||||
markup: props.noteBody,
|
||||
}, config, event);
|
||||
}, config);
|
||||
|
||||
logger.debug('Render complete.');
|
||||
} catch (error) {
|
||||
logger.error('Render failed:', error);
|
||||
}
|
||||
}, effectDependencies);
|
||||
|
||||
useEffect(() => {
|
||||
props.renderer.setExtraCss('edit-popup', editPopupCss);
|
||||
}, [editPopupCss, props.renderer]);
|
||||
};
|
||||
|
||||
export default useRerenderHandler;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user