You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
200 Commits
android-v3
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
227e41b69a | ||
|
a616e26a0f | ||
|
ba0e7e2226 | ||
|
b5a4ba554d | ||
|
9037da8f2d | ||
|
6998606ec9 | ||
|
66d52c90a3 | ||
|
f6fb1f7fbf | ||
|
3aac6043da | ||
|
ae170e0aa0 | ||
|
371f027a24 | ||
|
37422f316e | ||
|
a9f284ae45 | ||
|
fd2f69cc73 | ||
|
c4eab3c79c | ||
|
a0b9c6376e | ||
|
e2fc056369 | ||
|
453b4705b1 | ||
|
4128061e40 | ||
|
432b0ca870 | ||
|
c484cd2e48 | ||
|
58f0725c6b | ||
|
bf8fbec0cd | ||
|
f1d452f130 | ||
|
26012cd7d5 | ||
|
a414241541 | ||
|
0f13bf9d51 | ||
|
c142c5c5c0 | ||
|
af5c0135dc | ||
|
8a811b9e78 | ||
|
602484f143 | ||
|
dc84db1657 | ||
|
f5882ecfcc | ||
|
30000c34ec | ||
|
6e3df1bd90 | ||
|
67196ac0b2 | ||
|
69646b5522 | ||
|
9147afce9a | ||
|
c92701c52f | ||
|
ab3e9d1a3e | ||
|
f9cab8843b | ||
|
c36289c024 | ||
|
60b6db8cd4 | ||
|
bbd8f6f40e | ||
|
34b7f4e1f8 | ||
|
06b681d897 | ||
|
f02a94bef5 | ||
|
ae6b57c5a5 | ||
|
88ab916008 | ||
|
97b0ffc263 | ||
|
ff8848d138 | ||
|
2b686e6318 | ||
|
b913d18882 | ||
|
a2c9a01722 | ||
|
000d23c20f | ||
|
9e9f2f2930 | ||
|
c5a1a759c7 | ||
|
0b6a1c75ba | ||
|
53a0f8ddbc | ||
|
67eabb5038 | ||
|
983fced410 | ||
|
4f5bbc1132 | ||
|
2f10235ecb | ||
|
cfa7d6cb31 | ||
|
f5d62a50fe | ||
|
b52f5435aa | ||
|
bfd5bfc004 | ||
|
82965fe991 | ||
|
b2c162c25b | ||
|
022e76fe8d | ||
|
4b2d1895fd | ||
|
534507a31f | ||
|
5b4a300c81 | ||
|
1de0a59313 | ||
|
f4dff92d2e | ||
|
a5d37a0dca | ||
|
75ef418b39 | ||
|
6bd702ae24 | ||
|
9ea1808766 | ||
|
59f8dd36a6 | ||
|
ea1d2e4878 | ||
|
46ab00bfe4 | ||
|
07465dd349 | ||
|
a288ffe338 | ||
|
dba62386b6 | ||
|
6704ab0d13 | ||
|
0312f2213d | ||
|
2ac0b66ef6 | ||
|
639b261ee4 | ||
|
82bc819a21 | ||
|
72f8ebe4ff | ||
|
8c8a38e704 | ||
|
358134038c | ||
|
1f4b32a241 | ||
|
2a216f1e61 | ||
|
3f75d770f7 | ||
|
b6d32831c6 | ||
|
788033cb5f | ||
|
4e685ec687 | ||
|
c60b703b9c | ||
|
f23e10a975 | ||
|
b9a71c0c3d | ||
|
f525c4179f | ||
|
1dd0ec619f | ||
|
d2ee5411d0 | ||
|
a2472cb3b7 | ||
|
ca8415f74a | ||
|
853b792367 | ||
|
56d477f1c1 | ||
|
020ba10c56 | ||
|
be09873c58 | ||
|
4d8a16bda7 | ||
|
f725d3895f | ||
|
0e19dce0d1 | ||
|
31c5058d5e | ||
|
4d760303bc | ||
|
23e63e5fec | ||
|
3880352f53 | ||
|
42a3c40702 | ||
|
8e585640e7 | ||
|
cd3fb4e7ad | ||
|
702b5b3c63 | ||
|
a80406dcb7 | ||
|
ea8b6485d8 | ||
|
1a2ef78726 | ||
|
63d5ffc796 | ||
|
15918a57aa | ||
|
032e8b5596 | ||
|
ee091ede52 | ||
|
763e3f7479 | ||
|
0089c62493 | ||
|
20d6d56c02 | ||
|
8b999f8dc6 | ||
|
0067ac126d | ||
|
c6f47a9084 | ||
|
22817317f1 | ||
|
9ba1c0db4e | ||
|
70d6c1225c | ||
|
b1f013a8c2 | ||
|
8c66349907 | ||
|
86b4f713ee | ||
|
f50dc6a536 | ||
|
825ce51a3c | ||
|
c5b6f0bca1 | ||
|
86934d502e | ||
|
c63ad17f98 | ||
|
c746b5fdc2 | ||
|
949fb85755 | ||
|
0f94cb8c17 | ||
|
7ba61bb585 | ||
|
00e4657a39 | ||
|
cbdc98553a | ||
|
e3c2589a12 | ||
|
56b3cc3dc2 | ||
|
d59a09fd29 | ||
|
5a64222276 | ||
|
012297d52a | ||
|
5e70bce2c3 | ||
|
4c3eca1f18 | ||
|
c899f63a41 | ||
|
c838b86413 | ||
|
90d6d1747a | ||
|
6e8ba8a536 | ||
|
ffeb5f887a | ||
|
65bde86263 | ||
|
1c236ca73c | ||
|
2881280100 | ||
|
954b48b779 | ||
|
53e7b672b0 | ||
|
ceaaab77e8 | ||
|
c29bbe96f7 | ||
|
db323ac585 | ||
|
dc8e3242f3 | ||
|
9705941538 | ||
|
0cf9981ac7 | ||
|
b93ee3469b | ||
|
73e5bc74a5 | ||
|
6c761b3fb4 | ||
|
e13985a952 | ||
|
8b912b22cf | ||
|
4c90cd62fe | ||
|
999ec8c11f | ||
|
d8e73f3141 | ||
|
3b1a4e8209 | ||
|
1ff0f0f1c8 | ||
|
68863db4bd | ||
|
83f1fcc228 | ||
|
b03e370d2b | ||
|
4ddd5c4558 | ||
|
7746694dca | ||
|
f1ac95a1c7 | ||
|
78e9ced96c | ||
|
bba6ede569 | ||
|
7a26d4f336 | ||
|
04a976e459 | ||
|
ecfef1a9da | ||
|
e422a88bb0 | ||
|
74ef89d25b | ||
|
28b7251e16 | ||
|
9218c7df1f |
17
.env-sample
17
.env-sample
@@ -15,6 +15,23 @@
|
||||
# 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
|
||||
# -----------------------------------------------------------------------------
|
||||
|
@@ -10,13 +10,16 @@ QUEUE_TTL=900000
|
||||
QUEUE_RETRY_COUNT=2
|
||||
QUEUE_MAINTENANCE_INTERVAL=30000
|
||||
|
||||
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:0.0.2
|
||||
# Fullpath to images folder
|
||||
HTR_CLI_IMAGES_FOLDER=/home/user/joplin/packages/transcribe/images
|
||||
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:latest
|
||||
# Fullpath to images folder e.g.:
|
||||
#HTR_CLI_IMAGES_FOLDER=/home/user/joplin/packages/transcribe/images
|
||||
HTR_CLI_IMAGES_FOLDER=
|
||||
|
||||
QUEUE_DRIVER=pg
|
||||
# QUEUE_DRIVER=sqlite
|
||||
|
||||
FILE_STORAGE_MAINTENANCE_INTERVAL=3600000
|
||||
FILE_STORAGE_TTL=604800000 # one week
|
||||
|
||||
# =============================================================================
|
||||
# Queue driver
|
||||
@@ -27,4 +30,5 @@ QUEUE_DRIVER=pg
|
||||
QUEUE_DATABASE_NAME=transcribe
|
||||
QUEUE_DATABASE_USER=transcribe
|
||||
QUEUE_DATABASE_PASSWORD=transcribe
|
||||
QUEUE_DATABASE_PORT=5432
|
||||
QUEUE_DATABASE_PORT=5432
|
||||
QUEUE_DATABASE_HOST=localhost
|
157
.eslintignore
157
.eslintignore
@@ -55,6 +55,7 @@ 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/
|
||||
@@ -74,6 +75,7 @@ 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/
|
||||
@@ -96,6 +98,7 @@ 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
|
||||
@@ -133,6 +136,7 @@ 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
|
||||
@@ -155,6 +159,8 @@ 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
|
||||
@@ -195,6 +201,7 @@ 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
|
||||
@@ -296,6 +303,7 @@ 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
|
||||
@@ -663,6 +671,7 @@ 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
|
||||
@@ -671,48 +680,34 @@ 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/ImageEditor/utils/useEditorMessenger.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.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
|
||||
@@ -749,6 +744,7 @@ 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
|
||||
@@ -860,6 +856,38 @@ 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/markdownEditorBundle/utils/useCodeMirrorPlugins.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
|
||||
@@ -882,7 +910,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/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyAssets.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -891,6 +919,7 @@ 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
|
||||
@@ -918,7 +947,6 @@ 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
|
||||
@@ -976,24 +1004,47 @@ 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/ctrlClickActionExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.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/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/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testing/createTestEditor.js
|
||||
packages/editor/CodeMirror/testing/findNodesWithName.js
|
||||
packages/editor/CodeMirror/testing/forceFullParse.js
|
||||
@@ -1025,13 +1076,60 @@ 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/getCheckboxAtPosition.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/markdown/toggleCheckboxAt.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/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.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/createButton.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/forEachHeading.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.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
|
||||
@@ -1076,6 +1174,8 @@ 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
|
||||
@@ -1093,6 +1193,8 @@ 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
|
||||
@@ -1109,6 +1211,8 @@ 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
|
||||
@@ -1285,6 +1389,7 @@ 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
|
||||
@@ -1353,6 +1458,8 @@ 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
|
||||
@@ -1522,6 +1629,7 @@ 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
|
||||
@@ -1666,12 +1774,14 @@ packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
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
|
||||
@@ -1703,6 +1813,7 @@ 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,6 +23,7 @@ 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,9 +7,13 @@
|
||||
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
|
||||
|
||||
@@ -23,6 +27,10 @@ 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
|
||||
@@ -41,15 +49,17 @@ DOCKER_IMAGE_PLATFORM="linux/amd64"
|
||||
# a release
|
||||
RUN_TESTS=0
|
||||
|
||||
if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ]; then
|
||||
if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ] && [ "$IS_TRANSCRIBE_RELEASE" = 0 ]; then
|
||||
RUN_TESTS=1
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
@@ -80,12 +90,14 @@ 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"
|
||||
@@ -301,9 +313,13 @@ 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 Docker Image..."
|
||||
echo "Step: Building Joplin Server Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
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
|
||||
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,7 +17,6 @@ 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
|
||||
@@ -36,7 +35,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')
|
||||
if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/transcribe-v'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -141,7 +140,7 @@ jobs:
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
yarn install
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
|
||||
yarn buildServerDocker --docker-file Dockerfile.server --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.
|
||||
|
155
.gitignore
vendored
155
.gitignore
vendored
@@ -71,6 +71,7 @@ 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
|
||||
@@ -108,6 +109,7 @@ 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
|
||||
@@ -130,6 +132,8 @@ 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
|
||||
@@ -170,6 +174,7 @@ 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
|
||||
@@ -271,6 +276,7 @@ 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
|
||||
@@ -638,6 +644,7 @@ 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
|
||||
@@ -646,48 +653,34 @@ 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/ImageEditor/utils/useEditorMessenger.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.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
|
||||
@@ -724,6 +717,7 @@ 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
|
||||
@@ -835,6 +829,38 @@ 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/markdownEditorBundle/utils/useCodeMirrorPlugins.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
|
||||
@@ -857,7 +883,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/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyAssets.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -866,6 +892,7 @@ 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
|
||||
@@ -893,7 +920,6 @@ 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
|
||||
@@ -951,24 +977,47 @@ 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/ctrlClickActionExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.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/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/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testing/createTestEditor.js
|
||||
packages/editor/CodeMirror/testing/findNodesWithName.js
|
||||
packages/editor/CodeMirror/testing/forceFullParse.js
|
||||
@@ -1000,13 +1049,60 @@ 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/getCheckboxAtPosition.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/markdown/toggleCheckboxAt.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/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.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/createButton.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/forEachHeading.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.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
|
||||
@@ -1051,6 +1147,8 @@ 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
|
||||
@@ -1068,6 +1166,8 @@ 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
|
||||
@@ -1084,6 +1184,8 @@ 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
|
||||
@@ -1260,6 +1362,7 @@ 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
|
||||
@@ -1328,6 +1431,8 @@ 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
|
||||
@@ -1497,6 +1602,7 @@ 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
|
||||
@@ -1641,12 +1747,14 @@ packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
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
|
||||
@@ -1678,6 +1786,7 @@ 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
|
||||
|
BIN
Assets/WebsiteAssets/images/sponsors/DamanGame.png
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/DamanGame.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/EssayShark.png
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/EssayShark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
215
Assets/WebsiteAssets/locales/hr_HR.po
Normal file
215
Assets/WebsiteAssets/locales/hr_HR.po
Normal file
@@ -0,0 +1,215 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Milo Ivir <mail@mivirtype.de>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: hr_HR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.6\n"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:13
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:9
|
||||
msgid "/month"
|
||||
msgstr "/mjesec"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:19
|
||||
msgid "/year"
|
||||
msgstr "/godina"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:8
|
||||
msgid ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> allows you to "
|
||||
"synchronise your notes across devices. It also lets you publish notes, and "
|
||||
"collaborate on notebooks with your friends, family or colleagues."
|
||||
msgstr ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> omogućuje "
|
||||
"sinkronizaciju bilješki na različitim uređajima. Omogućuje i objavljivanje "
|
||||
"bilješki i suradnju na bilježnicama s prijateljima, obitelji ili kolegama."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:205
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow-lg\">Customise</span> it"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow-lg\">Prilagodi</span> uslugu"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:104
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span> notes"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">Multimedijske</span> bilješke"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:256
|
||||
msgid "100% <span class=\"frame-bg frame-bg-yellow-lg\">your data</span>"
|
||||
msgstr "100 % <span class=\"frame-bg frame-bg-yellow-lg\">tvoji podaci</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:298
|
||||
msgid "A <span class=\"frame-bg frame-bg-yellow-lg\">French</span> Alternative"
|
||||
msgstr ""
|
||||
"<span class=\"frame-bg frame-bg-yellow-lg\">Francuska</span> alternativa"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:236
|
||||
msgid ""
|
||||
"Access your notes from your computer, phone or tablet by synchronising with "
|
||||
"various services, including Joplin Cloud, Dropbox and OneDrive. The app is "
|
||||
"available on Windows, macOS, Linux, Android and iOS. A terminal app is also "
|
||||
"available!"
|
||||
msgstr ""
|
||||
"Pristupi svojim bilješkama s računala, mobitela ili tableta sinkronizacijom "
|
||||
"s raznim uslugama, uključujući Joplin Cloud, Dropbox i OneDrive. Program je "
|
||||
"dostupan za Windows, macOS, Linux, Android i iOS sustave. Dostupan je i "
|
||||
"program za terminal!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
msgid ""
|
||||
"Already have a Joplin Cloud account? <a href=\"https://"
|
||||
"joplincloud.com\">Login now</a>"
|
||||
msgstr ""
|
||||
"Već imaš Joplin Cloud račun? <a href=\"https://joplincloud.com\">Prijavi se "
|
||||
"sada</a>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:208
|
||||
msgid ""
|
||||
"Customise the app with plugins, custom themes and multiple text editors "
|
||||
"(Rich Text or Markdown). Or create your own scripts and plugins using the "
|
||||
"Extension API."
|
||||
msgstr ""
|
||||
"Prilagodi program pomoću dodataka, prilagođenih tema i uređivača teksta "
|
||||
"(formatirani tekst ili Markdown). Ili izradi vlastita skripta i dodatke "
|
||||
"pomoću API-ja za proširenja."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:242
|
||||
msgid "Download it now"
|
||||
msgstr "Preuzmi sada"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:112
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:63
|
||||
msgid "Download the app"
|
||||
msgstr "Preuzmi program"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:213
|
||||
msgid "Find out more"
|
||||
msgstr "Saznaj više"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:54
|
||||
msgid "Free your <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
msgstr "Oslobodi svoje <span class=\"frame-bg frame-bg-blue\">bilješke</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:175
|
||||
msgid "Get the clipper"
|
||||
msgstr "Nabavi Clipper"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:107
|
||||
msgid ""
|
||||
"Images, videos, PDFs and audio files are supported. Create math expressions "
|
||||
"and diagrams directly from the app. Take photos with the mobile app and save "
|
||||
"them to a note."
|
||||
msgstr ""
|
||||
"Podržane su slike, videozapisi, PDF-ovi i audio datoteke. Stvori matematičke "
|
||||
"izraze i dijagrame izravno iz programa. Snimaj fotografije s programom za "
|
||||
"mobitel i spremi ih u bilješku."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:327
|
||||
msgid "In the <span class=\"frame-bg frame-bg-yellow\">Press</span>"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">Recenzije</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:5
|
||||
msgid "Joplin Cloud <span class=\"frame-bg frame-bg-yellow\">plans</span>"
|
||||
msgstr "Joplin Cloud <span class=\"frame-bg frame-bg-yellow\">tarife</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:301
|
||||
msgid ""
|
||||
"Joplin Cloud is based in France. This means your data is protected by strict "
|
||||
"European Union privacy laws. In addition, Joplin Cloud implements strong end-"
|
||||
"to-end encryption so that not even us can have access to your data."
|
||||
msgstr ""
|
||||
"Joplin Cloud ima sjedište u Francuskoj. To znači da su tvoji podaci "
|
||||
"zaštićeni strogim zakonima o privatnosti Europske unije. Osim toga, Joplin "
|
||||
"Cloud implementira snažno sveobuhvatno šifriranje (end-to-end encryption) "
|
||||
"tako da čak ni mi ne možemo pristupiti tvojim podacima."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:57
|
||||
msgid ""
|
||||
"Joplin is an open source note-taking app. Capture your thoughts and securely "
|
||||
"access them from any device."
|
||||
msgstr ""
|
||||
"Joplin je program za bilješke otvorenog koda. Zabilježi svoje misli i "
|
||||
"sigurno im pristupi s bilo kojeg uređaja."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:262
|
||||
msgid "More about E2EE"
|
||||
msgstr "Više o E2EE"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:391
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
msgstr "Naši <span class=\"frame-bg frame-bg-blue-lg\">sponzori</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:23
|
||||
msgid "Pay Monthly"
|
||||
msgstr "Plaćaj mjesečno"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:30
|
||||
msgid "Pay Yearly"
|
||||
msgstr "Plaćaj godišnje"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:167
|
||||
msgid ""
|
||||
"Save <span class=\"frame-bg frame-bg-blue\">web pages</span> <br>as notes"
|
||||
msgstr ""
|
||||
"Spremaj <span class=\"frame-bg frame-bg-blue\">web stranice</span> <br>kao "
|
||||
"bilješke"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:65
|
||||
msgid "Sign up with Joplin Cloud"
|
||||
msgstr "Registriraj se na Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:394
|
||||
msgid "Thank you for your support!"
|
||||
msgstr "Hvala ti na podršci!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
msgid ""
|
||||
"The app is open source and your notes are saved to an open format, so you'll "
|
||||
"always have access to them. Uses End-To-End Encryption (E2EE) to secure your "
|
||||
"notes and ensure no-one but yourself can access them."
|
||||
msgstr ""
|
||||
"Program je otvorenog koda i tvoje se bilješke spremaju u otvorenom formatu, "
|
||||
"tako da ćeš im uvijek moći pristupiti. Program koristi sveobuhvatno "
|
||||
"šifriranje – engl. End-To-End Encryption (E2EE) – kako bi zaštitila tvoje "
|
||||
"bilješke i osigurala da im nitko osim tebe ne može pristupiti."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:144
|
||||
msgid "Try it now"
|
||||
msgstr "Isprobaj sada"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:170
|
||||
msgid ""
|
||||
"Use the web clipper extension, available on Chrome and Firefox, to save web "
|
||||
"pages or take screenshots as notes."
|
||||
msgstr ""
|
||||
"Koristi proširenje Web Clipper, dostupno za Chrome i Firefox, za spremanje "
|
||||
"web stranica ili snimanje ekrana kao bilješku."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
msgid ""
|
||||
"With Joplin Cloud, share your notes with your friends, family or colleagues "
|
||||
"and collaborate on them."
|
||||
msgstr ""
|
||||
"Joplin Cloud ti omogućuje da dijeliš bilješke s prijateljima, obitelji ili "
|
||||
"kolegama te da na njima surađujete."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:137
|
||||
msgid "Work <span class=\"frame-bg frame-bg-yellow\">together</span>"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">Surađuj</span> s drugima"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:141
|
||||
msgid ""
|
||||
"You can also publish a note to the internet and share the URL with others."
|
||||
msgstr "Bilješke možeš objaviti i na internetu te dijeliti URL s drugima."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:233
|
||||
msgid ""
|
||||
"Your notes, <span class=\"frame-bg frame-bg-blue-lg\">everywhere</span> you "
|
||||
"are"
|
||||
msgstr ""
|
||||
"Tvoje bilješke, <span class=\"frame-bg frame-bg-blue-lg\">gdje god</span> se "
|
||||
"nalaziš"
|
@@ -219,10 +219,7 @@
|
||||
$('.feature-description-' + featureId).toggle(200);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
const setHostingType = (type) => {
|
||||
const other = type === 'managed' ? 'self' : 'managed';
|
||||
$('.toggle-button-' + type).addClass('active');
|
||||
@@ -244,6 +241,7 @@
|
||||
setHostingType('self');
|
||||
});
|
||||
|
||||
setHostingType('managed');
|
||||
const initialHostingType = urlQuery.get('hosting') ? urlQuery.get('hosting') : 'managed';
|
||||
setHostingType(initialHostingType);
|
||||
</script>
|
||||
</div>
|
||||
|
@@ -23,7 +23,6 @@ RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY .yarn/plugins ./.yarn/plugins
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
COPY .yarn/patches ./.yarn/patches
|
||||
COPY package.json .
|
||||
|
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://homeworkguy.org/someone-to-take-my-online-class"><img title="someone to take my online class" width="256" src="https://joplinapp.org/images/sponsors/HomeworkGuy.png" alt="someone to take my online class"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://damangameplay.in"><img title="Daman Game" width="256" src="https://joplinapp.org/images/sponsors/DamanGame.png" alt="Daman Game"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
@@ -40,9 +40,8 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) |
|
||||
| | | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
# Community
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"version": "latest",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"yarn": "latest",
|
||||
"yarn": "1.22.19",
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
@@ -16,13 +16,13 @@
|
||||
"version": "",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"python": "3.13.2",
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
"electron": {
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "2.47.2",
|
||||
"git": "2.48.1",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
@@ -17,11 +17,21 @@
|
||||
|
||||
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
|
||||
@@ -31,10 +41,17 @@ 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
|
||||
@@ -45,3 +62,48 @@ 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}
|
||||
|
||||
|
13
fastlane/metadata/android/hr/full_description.txt
Normal file
13
fastlane/metadata/android/hr/full_description.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
<strong>Joplin</strong> je besplatan program otvorenog koda za bilješke i popis zadataka koji može obraditi veliki broj bilješki organizirane u bilježnice. Bilješke se mogu pretraživati, kopirati, označavati i mijenjati izravno iz programa ili iz vlastitog uređivača teksta.
|
||||
|
||||
Bilješke su u <a href="https://joplinapp.org/help/apps/markdown">Markdown formatu</a>.
|
||||
|
||||
Iz Evernotea izvezene bilješke <a href="https://joplinapp.org/help/apps/import_export">mogu se uvesti</a> u Joplin, uključujući formatirani sadržaj (koji se pretvara u Markdown), resurse (slike, privitke itd.) i potpune metapodatke (geografski podaci mjesta, vrijeme aktualiziranja, vrijeme stvaranja itd.). Mogu se uvesti i obične Markdown datoteke.
|
||||
|
||||
Joplin radi ponajprije s lokalnim podacima (offline first), što znači da uvijek imaš sve svoje podatke na mobitelu ili računalu. To osigurava da su tvoje bilješke uvijek dostupne, bez obzira je li imaš internetsku vezu ili ne.</p>
|
||||
|
||||
Bilješke se mogu sigurno <a href="https://joplinapp.org/help/apps/sync">sinkronizirati</a> pomoću <a href="https://joplinapp.org/help/apps/sync/e2ee">sveobuhvatnog šifriranja</a> s raznim uslugama u oblaku, uključujući Nextcloud, Dropbox, OneDrive i <a href="https://joplinapp.org/plans/">Joplin Cloud</a>.
|
||||
|
||||
Pretraživanje cijelog teksta dostupno je na svim platformama za brzo pronalaženje potrebnih informacija. Program se može prilagoditi pomoću dodataka i tema, a možeš stvoriti i vlastite.
|
||||
|
||||
Program je dostupan za Windows, Linux, macOS, Android i iOS sustave. <a href="https://joplinapp.org/help/apps/clipper">Web Clipper</a>, za spremanje web stranica i snimaka ekrana iz tvog preglednika, je također dostupan za <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/">Firefox</a> i <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek">Chrome</a>.
|
1
fastlane/metadata/android/hr/short_description.txt
Normal file
1
fastlane/metadata/android/hr/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Program za bilješke i popis zadataka sa sinkronizacijom između Linuxa, macOS-a, Windowsa i mobitela
|
16
package.json
16
package.json
@@ -51,6 +51,8 @@
|
||||
"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",
|
||||
@@ -72,22 +74,22 @@
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.2.0",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.2",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.5.1",
|
||||
"lint-staged": "15.5.2",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.4.5"
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.4",
|
||||
@@ -100,7 +102,7 @@
|
||||
"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.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.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",
|
||||
|
@@ -380,6 +380,13 @@ class AppGui {
|
||||
this.widget('noteList').toggleShowIds();
|
||||
}
|
||||
|
||||
toggleFolderCollapse() {
|
||||
const folderList = this.widget('folderList');
|
||||
if (folderList && folderList.toggleFolderCollapse) {
|
||||
folderList.toggleFolderCollapse();
|
||||
}
|
||||
}
|
||||
|
||||
widget(name) {
|
||||
if (name === 'root') return this.rootWidget_;
|
||||
return this.rootWidget_.childByName(name);
|
||||
@@ -506,6 +513,8 @@ class AppGui {
|
||||
this.toggleNoteMetadata();
|
||||
} else if (cmd === 'toggle_ids') {
|
||||
this.toggleFolderIds();
|
||||
} else if (cmd === 'toggle_folder_collapse') {
|
||||
this.toggleFolderCollapse();
|
||||
} else if (cmd === 'enter_command_line_mode') {
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
|
@@ -9,7 +9,6 @@ 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';
|
||||
@@ -19,7 +18,6 @@ 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 {
|
||||
|
||||
@@ -222,6 +220,7 @@ class Application extends BaseApplication {
|
||||
return { ...this.commandMetadata_ };
|
||||
}
|
||||
|
||||
|
||||
public hasGui() {
|
||||
return this.gui() && !this.gui().isDummy();
|
||||
}
|
||||
@@ -332,6 +331,7 @@ class Application extends BaseApplication {
|
||||
{ keys: ['mb'], type: 'prompt', command: 'mkbook ""', cursorPosition: -2 },
|
||||
{ keys: ['yn'], type: 'prompt', command: 'cp $n ""', cursorPosition: -2 },
|
||||
{ keys: ['dn'], type: 'prompt', command: 'mv $n ""', cursorPosition: -2 },
|
||||
{ keys: ['z'], type: 'function', command: 'toggle_folder_collapse' },
|
||||
];
|
||||
|
||||
// Filter the keymap item by command so that items in keymap.json can override
|
||||
@@ -381,22 +381,6 @@ 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[]) {
|
||||
@@ -433,15 +417,10 @@ 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 {
|
||||
const commands = await this.commandList(argv);
|
||||
for (const command of commands) {
|
||||
await this.execCommand(command);
|
||||
}
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
if (this.showStackTraces_) {
|
||||
console.error(error);
|
||||
|
@@ -1,19 +0,0 @@
|
||||
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;
|
79
packages/app-cli/app/command-batch.ts
Normal file
79
packages/app-cli/app/command-batch.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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;
|
@@ -6,6 +6,7 @@ import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ImportOptions } from '@joplin/lib/services/interop/types';
|
||||
import { unique } from '@joplin/lib/array';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -32,14 +33,16 @@ class Command extends BaseCommand {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public override async action(args: any) {
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
|
||||
let destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
|
||||
|
||||
if (args.notebook && !folder) throw new Error(_('Cannot find "%s".', args.notebook));
|
||||
if (args.notebook && !destinationFolder) throw new Error(_('Cannot find "%s".', args.notebook));
|
||||
|
||||
if (!destinationFolder) destinationFolder = await Folder.defaultFolder();
|
||||
|
||||
const importOptions: ImportOptions = {};
|
||||
importOptions.path = args.path;
|
||||
importOptions.format = args.options.format ? args.options.format : 'auto';
|
||||
importOptions.destinationFolderId = folder ? folder.id : null;
|
||||
importOptions.destinationFolderId = destinationFolder ? destinationFolder.id : null;
|
||||
|
||||
let lastProgress = '';
|
||||
|
||||
|
@@ -14,17 +14,25 @@ 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: stdoutFn,
|
||||
warn: stdoutFn,
|
||||
info: args.options.quiet ? ignoreOutputFn : stdoutFn,
|
||||
warn: args.options.quiet ? ignoreOutputFn : stdoutFn,
|
||||
error: stdoutFn,
|
||||
} });
|
||||
ClipperServer.instance().setDispatch(() => {});
|
||||
@@ -38,7 +46,11 @@ 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');
|
||||
await ClipperServer.instance().start(); // Never exit
|
||||
const promise = ClipperServer.instance().start();
|
||||
|
||||
if (!args.options['exit-early']) {
|
||||
await promise; // Never exit
|
||||
}
|
||||
}
|
||||
} else if (command === 'status') {
|
||||
this.stdout(runningOnPort ? _('Server is running on port %d', runningOnPort) : _('Server is not running.'));
|
||||
|
@@ -149,6 +149,7 @@ class Command extends BaseCommand {
|
||||
waiting: invitation.status === ShareUserStatus.Waiting,
|
||||
rejected: invitation.status === ShareUserStatus.Rejected,
|
||||
folderId: invitation.share.folder_id,
|
||||
canWrite: !!invitation.can_write,
|
||||
fromUser: {
|
||||
email: invitation.share.user?.email,
|
||||
},
|
||||
|
@@ -2,6 +2,7 @@ import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -20,6 +21,18 @@ class Command extends BaseCommand {
|
||||
public override async action(args: any) {
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
|
||||
|
||||
// Auto-expand parent folders in GUI if present
|
||||
if (app().gui() && app().gui().widget && app().gui().widget('folderList')) {
|
||||
const folderListWidget = app().gui().widget('folderList');
|
||||
if (folderListWidget.expandToFolder) {
|
||||
// Get all folders to pass to expandToFolder
|
||||
const folders = await Folder.all();
|
||||
folderListWidget.folders = folders; // Ensure widget has current folders
|
||||
folderListWidget.expandToFolder(folder.id);
|
||||
}
|
||||
}
|
||||
|
||||
app().switchCurrentFolder(folder);
|
||||
}
|
||||
}
|
||||
|
@@ -4,11 +4,14 @@ import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { getDisplayParentId, getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
import {
|
||||
getDisplayParentId,
|
||||
getTrashFolderId,
|
||||
} from '@joplin/lib/services/trash';
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
|
||||
export default class FolderListWidget extends ListWidget {
|
||||
|
||||
export default class FolderListWidget extends ListWidget {
|
||||
private folders_: FolderEntity[] = [];
|
||||
|
||||
public constructor() {
|
||||
@@ -31,7 +34,18 @@ export default class FolderListWidget extends ListWidget {
|
||||
if (item === '-') {
|
||||
output.push('-'.repeat(this.innerWidth));
|
||||
} else if (item.type_ === Folder.modelType()) {
|
||||
output.push(' '.repeat(this.folderDepth(this.folders, item.id)));
|
||||
const depth = this.folderDepth(this.folders, item.id);
|
||||
output.push(' '.repeat(depth));
|
||||
|
||||
// Add collapse/expand indicator
|
||||
const hasChildren = this.folderHasChildren_(this.folders, item.id);
|
||||
if (hasChildren) {
|
||||
const collapsedFolders = Setting.value('collapsedFolderIds');
|
||||
const isCollapsed = collapsedFolders.includes(item.id);
|
||||
output.push(isCollapsed ? '[+] ' : '[-] ');
|
||||
} else {
|
||||
output.push(' '); // Space for alignment
|
||||
}
|
||||
|
||||
if (this.showIds) {
|
||||
output.push(Folder.shortId(item.id));
|
||||
@@ -65,7 +79,10 @@ export default class FolderListWidget extends ListWidget {
|
||||
let output = 0;
|
||||
while (true) {
|
||||
const folder = BaseModel.byId(folders, folderId);
|
||||
const folderParentId = getDisplayParentId(folder, folders.find(f => f.id === folder.parent_id));
|
||||
const folderParentId = getDisplayParentId(
|
||||
folder,
|
||||
folders.find((f) => f.id === folder.parent_id),
|
||||
);
|
||||
if (!folder || !folderParentId) return output;
|
||||
output++;
|
||||
folderId = folderParentId;
|
||||
@@ -153,7 +170,10 @@ export default class FolderListWidget extends ListWidget {
|
||||
public folderHasChildren_(folders: FolderEntity[], folderId: string) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
const folderParentId = getDisplayParentId(folder, folders.find(f => f.id === folder.parent_id));
|
||||
const folderParentId = getDisplayParentId(
|
||||
folder,
|
||||
folders.find((f) => f.id === folder.parent_id),
|
||||
);
|
||||
if (folderParentId === folderId) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -161,7 +181,12 @@ export default class FolderListWidget extends ListWidget {
|
||||
|
||||
public render() {
|
||||
if (this.updateItems_) {
|
||||
this.logger().debug('Rebuilding items...', this.notesParentType, this.selectedJoplinItemId, this.selectedSearchId);
|
||||
this.logger().debug(
|
||||
'Rebuilding items...',
|
||||
this.notesParentType,
|
||||
this.selectedJoplinItemId,
|
||||
this.selectedSearchId,
|
||||
);
|
||||
const wasSelectedItemId = this.selectedJoplinItemId;
|
||||
const previousParentType = this.notesParentType;
|
||||
|
||||
@@ -170,12 +195,20 @@ export default class FolderListWidget extends ListWidget {
|
||||
const orderFolders = (parentId: string) => {
|
||||
for (let i = 0; i < this.folders.length; i++) {
|
||||
const f = this.folders[i];
|
||||
const originalParent = this.folders_.find(f => f.id === f.parent_id);
|
||||
const originalParent = this.folders_.find(
|
||||
(f) => f.id === f.parent_id,
|
||||
);
|
||||
|
||||
const folderParentId = getDisplayParentId(f, originalParent); // f.parent_id ? f.parent_id : '';
|
||||
if (folderParentId === parentId) {
|
||||
newItems.push(f);
|
||||
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
|
||||
// Only recurse into children if the folder is not collapsed
|
||||
if (this.folderHasChildren_(this.folders, f.id)) {
|
||||
const collapsedFolders = Setting.value('collapsedFolderIds');
|
||||
if (!collapsedFolders.includes(f.id)) {
|
||||
orderFolders(f.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -221,4 +254,53 @@ export default class FolderListWidget extends ListWidget {
|
||||
const index = this.itemIndexByKey('id', itemId);
|
||||
this.currentIndex = index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
public toggleFolderCollapse() {
|
||||
const item = this.currentItem;
|
||||
if (item && item.type_ === Folder.modelType() && this.folderHasChildren_(this.folders, item.id)) {
|
||||
const collapsedFolders = Setting.value('collapsedFolderIds');
|
||||
const isCollapsed = collapsedFolders.includes(item.id);
|
||||
if (isCollapsed) {
|
||||
const newCollapsed = collapsedFolders.filter((id: string) => id !== item.id);
|
||||
Setting.setValue('collapsedFolderIds', newCollapsed);
|
||||
} else {
|
||||
Setting.setValue('collapsedFolderIds', [...collapsedFolders, item.id]);
|
||||
}
|
||||
this.updateItems_ = true;
|
||||
this.invalidate();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public expandToFolder(folderId: string) {
|
||||
// Find all parent folders and expand them
|
||||
const parentsToExpand: string[] = [];
|
||||
let currentId = folderId;
|
||||
|
||||
while (currentId) {
|
||||
const folder = BaseModel.byId(this.folders, currentId);
|
||||
if (!folder) break;
|
||||
|
||||
const parentId = getDisplayParentId(
|
||||
folder,
|
||||
this.folders.find((f) => f.id === folder.parent_id),
|
||||
);
|
||||
if (parentId) {
|
||||
parentsToExpand.unshift(parentId);
|
||||
currentId = parentId;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand all parent folders
|
||||
const collapsedFolders = Setting.value('collapsedFolderIds');
|
||||
const newCollapsed = collapsedFolders.filter((id: string) => !parentsToExpand.includes(id));
|
||||
Setting.setValue('collapsedFolderIds', newCollapsed);
|
||||
|
||||
this.updateItems_ = true;
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
|
54
packages/app-cli/app/utils/iterateStdin.ts
Normal file
54
packages/app-cli/app/utils/iterateStdin.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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.33.5",
|
||||
"sharp": "0.34.2",
|
||||
"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.12",
|
||||
"@types/node": "18.19.86",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.4.5"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
|
||||
import MarkupToHtml, { MarkupLanguage } from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult } from '@joplin/renderer/types';
|
||||
import MarkupToHtml from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult, MarkupLanguage } from '@joplin/renderer/types';
|
||||
|
||||
describe('MarkupToHtml', () => {
|
||||
|
||||
|
@@ -0,0 +1,13 @@
|
||||
<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>
|
@@ -0,0 +1,5 @@
|
||||
A task list created by the TipTap editor:
|
||||
|
||||
- [ ] Testing...
|
||||
|
||||
- [ ] testing
|
26
packages/app-cli/tests/html_to_md/task_lists.html
Normal file
26
packages/app-cli/tests/html_to_md/task_lists.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
15
packages/app-cli/tests/html_to_md/task_lists.md
Normal file
15
packages/app-cli/tests/html_to_md/task_lists.md
Normal file
@@ -0,0 +1,15 @@
|
||||
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">
|
||||
<ul class="joplin-checklist" data-is-checklist="1">
|
||||
<li>Not checked</li>
|
||||
<li class="checked">Checked!!
|
||||
<ul class="joplin-checklist">
|
||||
<ul class="joplin-checklist" data-is-checklist="1">
|
||||
<li class="checked">Indented, with <strong>bold</strong></li>
|
||||
<li>Indented, not checked</li>
|
||||
</ul>
|
||||
|
@@ -1,15 +1,15 @@
|
||||
<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,
|
||||
<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,
|
||||
		<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>
|
||||
	"/></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,
|
||||
	"/></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,
|
||||
		<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>
|
||||
	"/></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,
|
||||
	"/></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,
|
||||
		<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>
|
||||
	"/></div>
|
||||
	"/></span>
|
@@ -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';
|
||||
const url = require('url');
|
||||
import * as url from 'url';
|
||||
const path = require('path');
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
|
@@ -55,11 +55,16 @@ 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,
|
||||
@@ -67,6 +72,8 @@ 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
|
||||
@@ -348,16 +355,19 @@ 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 driver = new OcrDriverTesseract(
|
||||
const drivers: OcrDriverBase[] = [];
|
||||
drivers.push(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,
|
||||
},
|
||||
);
|
||||
));
|
||||
|
||||
this.ocrService_ = new OcrService(driver);
|
||||
drivers.push(new OcrDriverTranscribe());
|
||||
|
||||
this.ocrService_ = new OcrService(drivers);
|
||||
}
|
||||
|
||||
void this.ocrService_.runInBackground();
|
||||
@@ -411,56 +421,53 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
|
||||
// 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, '.');
|
||||
private buildStartupTasks_() {
|
||||
const tasks: StartupTask[] = [];
|
||||
const addTask = (label: string, task: StartupTask['task']) => {
|
||||
tasks.push({ label, task });
|
||||
};
|
||||
|
||||
argv = await super.start(argv, startOptions);
|
||||
addTask('app/set up extra debug logging', () => {
|
||||
reg.logger().info('app.start: doing regular boot');
|
||||
const dir: string = Setting.value('profileDir');
|
||||
|
||||
await this.setupIntegrationTestUtils();
|
||||
syncDebugLog.enabled = false;
|
||||
|
||||
bridge().setLogFilePath(Logger.globalLogger.logFilePath());
|
||||
if (dir.endsWith('dev-desktop-2')) {
|
||||
syncDebugLog.addTarget(TargetType.File, {
|
||||
path: `${homedir()}/synclog.txt`,
|
||||
});
|
||||
syncDebugLog.enabled = true;
|
||||
syncDebugLog.info(`Profile dir: ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
await this.applySettingsSideEffects();
|
||||
addTask('app/set up registry', () => {
|
||||
reg.setDispatch(this.dispatch.bind(this));
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
|
||||
});
|
||||
|
||||
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 auto updater', () => {
|
||||
this.setupAutoUpdaterService();
|
||||
});
|
||||
|
||||
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); });
|
||||
addTask('app/set up AlarmService', () => {
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
});
|
||||
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
bridge().openDevTools();
|
||||
addTask('app/openDevTools', () => {
|
||||
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'));
|
||||
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'));
|
||||
});
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('tempDir'));
|
||||
// For now, this doesn't seem necessary:
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('profileDir'));
|
||||
@@ -468,44 +475,52 @@ class Application extends BaseApplication {
|
||||
// handler, and, as such, it may make sense to also limit permissions of
|
||||
// allowed pages with a Content Security Policy.
|
||||
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
PluginManager.instance().setLogger(reg.logger());
|
||||
PluginManager.instance().register(pluginClasses);
|
||||
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);
|
||||
|
||||
this.initRedux();
|
||||
this.initRedux();
|
||||
|
||||
PerFolderSortOrderService.initialize();
|
||||
initializeCommandService(this.store(), Setting.value('env') === 'dev');
|
||||
|
||||
initializeCommandService(this.store(), Setting.value('env') === 'dev');
|
||||
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());
|
||||
|
||||
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,
|
||||
try {
|
||||
await keymapService.loadCustomKeymap(`${Setting.value('profileDir')}/keymap-desktop.json`);
|
||||
} catch (error) {
|
||||
reg.logger().error(error);
|
||||
}
|
||||
});
|
||||
|
||||
await this.setupCustomCss();
|
||||
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();
|
||||
});
|
||||
|
||||
// const masterKeys = await MasterKey.all();
|
||||
|
||||
@@ -514,188 +529,237 @@ class Application extends BaseApplication {
|
||||
// items: masterKeys,
|
||||
// });
|
||||
|
||||
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'),
|
||||
};
|
||||
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,
|
||||
});
|
||||
}
|
||||
return notesParent;
|
||||
};
|
||||
|
||||
const notesParent = await getNotesParent();
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
if (notesParent.type === 'SmartFilter') {
|
||||
this.store().dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
});
|
||||
} else if (notesParent.type === 'Tag') {
|
||||
this.store().dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
addTask('app/initializeUserFetcher', async () => {
|
||||
initializeUserFetcher();
|
||||
shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
});
|
||||
|
||||
// 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/updateTray', () => this.updateTray());
|
||||
|
||||
// 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/set main window state', () => {
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().mainWindow().hide();
|
||||
} else {
|
||||
bridge().mainWindow().show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
initializeUserFetcher();
|
||||
shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
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);
|
||||
}
|
||||
|
||||
this.updateTray();
|
||||
// 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') });
|
||||
}
|
||||
};
|
||||
|
||||
shim.setTimeout(() => {
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
// Initial check on startup
|
||||
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
|
||||
// Then every x hours
|
||||
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().mainWindow().hide();
|
||||
} else {
|
||||
bridge().mainWindow().show();
|
||||
}
|
||||
shim.setTimeout(() => {
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
void ShareService.instance().maintenance();
|
||||
|
||||
void ShareService.instance().maintenance();
|
||||
ResourceService.runInBackground();
|
||||
|
||||
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.
|
||||
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();
|
||||
|
||||
void DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
}
|
||||
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();
|
||||
this.startRotatingLogMaintenance(Setting.value('profileDir'));
|
||||
});
|
||||
|
||||
RevisionService.instance().runInBackground();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
||||
if (Setting.value('env') === 'dev') {
|
||||
// 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/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_,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
|
||||
bridge().setOnAllowedExtensionsChangeListener((newExtensions) => {
|
||||
Setting.setValue('linking.extraAllowedExtensions', newExtensions);
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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/initPluginService', () => this.initPluginService());
|
||||
|
||||
addTask('app/setupContextMenu', () => {
|
||||
this.setupContextMenu();
|
||||
});
|
||||
|
||||
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/set up SpellCheckerService', async () => {
|
||||
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
|
||||
});
|
||||
|
||||
eventManager.on(EventName.NoteResourceIndexed, async () => {
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
addTask('app/listen for resource events', () => {
|
||||
eventManager.on(EventName.OcrServiceResourcesProcessed, async () => {
|
||||
await ResourceService.instance().indexNoteResources();
|
||||
});
|
||||
|
||||
eventManager.on(EventName.NoteResourceIndexed, async () => {
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
});
|
||||
});
|
||||
|
||||
// Used by tests
|
||||
ipcRenderer.send('startup-finished');
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
// setTimeout(() => {
|
||||
// void populateDatabase(reg.db(), {
|
||||
@@ -749,6 +813,10 @@ 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 } from 'fs-extra';
|
||||
import { extname, normalize } from 'path';
|
||||
import { pathExists, pathExistsSync, writeFileSync, ensureDirSync } from 'fs-extra';
|
||||
import { extname, normalize, join } from 'path';
|
||||
import isSafeToOpen from './utils/isSafeToOpen';
|
||||
import { closeSync, openSync, readSync, statSync } from 'fs';
|
||||
import { KB } from '@joplin/utils/bytes';
|
||||
@@ -67,6 +67,30 @@ 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 {
|
||||
@@ -109,7 +133,10 @@ export class Bridge {
|
||||
log: logAttachment ? logAttachment.data.trim().split('\n') : [],
|
||||
};
|
||||
|
||||
writeFileSync(`${homedir()}/joplin_crash_dump_${date}.json`, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8');
|
||||
const crashDumpDir = this.getCrashDumpDirectory();
|
||||
ensureDirSync(crashDumpDir);
|
||||
const crashDumpPath = join(crashDumpDir, `joplin_crash_dump_${date}.json`);
|
||||
writeFileSync(crashDumpPath, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8');
|
||||
} catch (error) {
|
||||
// Ignore the error since we can't handle it here
|
||||
}
|
||||
|
96
packages/app-desktop/commands/convertNoteToMarkdown.test.ts
Normal file
96
packages/app-desktop/commands/convertNoteToMarkdown.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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');
|
||||
});
|
||||
|
||||
});
|
52
packages/app-desktop/commands/convertNoteToMarkdown.ts
Normal file
52
packages/app-desktop/commands/convertNoteToMarkdown.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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,4 +1,5 @@
|
||||
// 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';
|
||||
@@ -24,6 +25,7 @@ import * as toggleSafeMode from './toggleSafeMode';
|
||||
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
||||
|
||||
const index: any[] = [
|
||||
convertNoteToMarkdown,
|
||||
copyDevCommand,
|
||||
copyToClipboard,
|
||||
editProfileConfig,
|
||||
|
@@ -0,0 +1,28 @@
|
||||
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 } from 'react';
|
||||
import { useMemo, useRef, useState, useCallback } from 'react';
|
||||
|
||||
interface Props {
|
||||
width: number;
|
||||
@@ -9,40 +9,62 @@ interface Props {
|
||||
const fontSizeCache_: Record<string, number> = {};
|
||||
|
||||
export default (props: Props) => {
|
||||
const containerRef = useRef(null);
|
||||
const containerRef = useRef<HTMLDivElement>(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) return props.height;
|
||||
if (!containerReady || !containerRef.current) {
|
||||
return Math.min(props.height * 0.7, 14);
|
||||
}
|
||||
|
||||
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) {
|
||||
spanFontSize -= .5;
|
||||
while ((rect.height > props.height || rect.width > props.width) && spanFontSize > 1) {
|
||||
spanFontSize -= 0.5;
|
||||
span.style.fontSize = `${spanFontSize}px`;
|
||||
rect = span.getBoundingClientRect();
|
||||
}
|
||||
|
||||
span.remove();
|
||||
|
||||
fontSizeCache_[cacheKey] = spanFontSize;
|
||||
return spanFontSize;
|
||||
}, [props.width, props.height, props.emoji, containerReady, containerRef]);
|
||||
}, [props.width, props.height, props.emoji, containerReady]);
|
||||
|
||||
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>;
|
||||
return <div
|
||||
ref={refCallback}
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
fontSize,
|
||||
}}
|
||||
>
|
||||
{props.emoji}
|
||||
</div>;
|
||||
};
|
||||
|
@@ -38,12 +38,14 @@ 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;
|
||||
|
||||
@@ -84,6 +86,7 @@ interface Props {
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
toast: Toast;
|
||||
shouldSwitchToAppleSiliconVersion: boolean;
|
||||
noteHtmlToMarkdownDone: string;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -797,6 +800,10 @@ 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}
|
||||
@@ -853,6 +860,7 @@ 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,6 +803,7 @@ function useMenu(props: Props) {
|
||||
menuItemDic.toggleNoteList,
|
||||
menuItemDic.toggleVisiblePanes,
|
||||
menuItemDic.toggleEditorPlugin,
|
||||
menuItemDic.toggleEditors,
|
||||
{
|
||||
label: _('Layout button sequence'),
|
||||
submenu: layoutButtonSequenceMenuItems,
|
||||
@@ -906,6 +907,7 @@ function useMenu(props: Props) {
|
||||
separator(),
|
||||
menuItemDic.setTags,
|
||||
menuItemDic.showShareNoteDialog,
|
||||
menuItemDic.convertNoteToMarkdown,
|
||||
separator(),
|
||||
menuItemDic.showNoteProperties,
|
||||
menuItemDic.showNoteContentProperties,
|
||||
|
@@ -340,6 +340,8 @@ 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]);
|
||||
|
||||
@@ -362,6 +364,8 @@ 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,
|
||||
@@ -410,6 +414,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
onSelectPastBeginning={onSelectPastBeginning}
|
||||
externalSearch={props.searchMarkers}
|
||||
useLocalSearch={props.useLocalSearch}
|
||||
onLocalize={_}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -15,6 +15,10 @@ 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;
|
||||
@@ -104,7 +108,16 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
onLogMessage: message => onLogMessageRef.current(message),
|
||||
};
|
||||
|
||||
const editor = createEditor(editorContainerRef.current, editorProps);
|
||||
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);
|
||||
if (!item) return null;
|
||||
return `${getResourceBaseUrl()}/${resourceFilename(item)}`;
|
||||
},
|
||||
});
|
||||
editor.addStyles({
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
'&.CodeMirror': {
|
||||
|
@@ -5,6 +5,7 @@ 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,6 +56,7 @@ 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');
|
||||
|
||||
@@ -169,7 +170,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml(props.plugins, {
|
||||
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
|
||||
resourceBaseUrl: getResourceBaseUrl(),
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
@@ -466,6 +467,7 @@ 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;
|
||||
@@ -488,6 +490,17 @@ 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');
|
||||
@@ -632,9 +645,30 @@ 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
|
||||
@@ -722,6 +756,7 @@ 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,7 +4,8 @@ import { AppState } from '../../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import BannerContent from './BannerContent';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../services/bridge';
|
||||
import onRichTextReadMoreLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick';
|
||||
import onRichTextDismissLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick';
|
||||
import { useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
@@ -16,14 +17,6 @@ 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,6 +69,10 @@ export default function styles(props: NoteEditorProps) {
|
||||
marginTop: 0,
|
||||
marginBottom: 10,
|
||||
},
|
||||
resourceWatchBannerAction: {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colorWarnUrl,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -8,14 +8,15 @@ 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, ResourceOcrStatus } from '@joplin/lib/services/database/types';
|
||||
import { NoteEntity, ResourceEntity, ResourceOcrDriverId, 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 from '@joplin/lib/shim';
|
||||
import shim, { MessageBoxType } 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');
|
||||
@@ -137,6 +138,40 @@ 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) => {
|
||||
|
@@ -0,0 +1,4 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const getResourceBaseUrl = () => `joplin-content://note-viewer/${Setting.value('resourceDir')}/`;
|
||||
export default getResourceBaseUrl;
|
@@ -67,6 +67,7 @@ export interface NoteEditorProps {
|
||||
onTitleChange?: (title: string)=> void;
|
||||
bodyEditor: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
enableHtmlToMarkdownBanner: boolean;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorRef {
|
||||
@@ -138,6 +139,7 @@ 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) return;
|
||||
if (!formNote.id || !formNote.bodyWillChangeId || !props.editorRef.current) return;
|
||||
|
||||
const body = await props.editorRef.current.content();
|
||||
|
||||
|
@@ -343,6 +343,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
style={styles.input}
|
||||
id={uniqueId(key)}
|
||||
name={uniqueId(key)}
|
||||
autoFocus
|
||||
/>;
|
||||
|
||||
editCompHandler = () => {
|
||||
@@ -363,6 +364,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
id={uniqueId(key)}
|
||||
name={uniqueId(key)}
|
||||
aria-invalid={!this.state.isValid.location}
|
||||
autoFocus
|
||||
/>
|
||||
{
|
||||
this.state.isValid.location ? null
|
||||
@@ -387,6 +389,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
style={styles.input}
|
||||
id={uniqueId(key)}
|
||||
name={uniqueId(key)}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -117,6 +118,15 @@ 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) => {
|
||||
@@ -256,7 +266,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
onChange={onChange}
|
||||
type="datetime-local"
|
||||
className='datetime-picker'
|
||||
style={styles.input}
|
||||
style={styles.dateTimeInput}
|
||||
/>;
|
||||
} 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'>
|
||||
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall' title={label}>
|
||||
<i
|
||||
aria-label={label}
|
||||
role='img'
|
||||
@@ -39,9 +39,11 @@ 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'.
|
||||
return <button onClick={onAddFolderButtonClick} className='sidebar-header-button -newfolder'>
|
||||
const label = _('New notebook');
|
||||
|
||||
return <button onClick={onAddFolderButtonClick} className='sidebar-header-button -newfolder' title={label}>
|
||||
<i
|
||||
aria-label={_('New notebook')}
|
||||
aria-label={label}
|
||||
role='img'
|
||||
className='fas fa-plus'
|
||||
/>
|
||||
|
@@ -79,5 +79,7 @@ export default function() {
|
||||
'switchProfile3',
|
||||
'pasteAsText',
|
||||
'showNoteProperties',
|
||||
'convertNoteToMarkdown',
|
||||
'toggleEditors',
|
||||
];
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ 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 pdfJs = require('pdfjs-dist');
|
||||
const { isAppleSilicon } = require('is-apple-silicon');
|
||||
require('@sentry/electron/renderer');
|
||||
@@ -38,6 +39,8 @@ 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
|
||||
@@ -106,7 +109,7 @@ const main = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
perfLogger.track('main', main).catch((error) => {
|
||||
const env = bridge().env();
|
||||
console.error(error);
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.4.2",
|
||||
"version": "3.4.7",
|
||||
"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.1",
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.4",
|
||||
@@ -142,14 +142,14 @@
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@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/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/react": "18.3.22",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
@@ -160,9 +160,9 @@
|
||||
"compare-versions": "6.1.1",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron": "35.5.1",
|
||||
"electron": "35.7.5",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.0",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
@@ -179,7 +179,6 @@
|
||||
"moment": "2.30.1",
|
||||
"mustache": "4.2.0",
|
||||
"nan": "2.22.2",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-rsa": "1.1.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
@@ -202,15 +201,16 @@
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.1.1",
|
||||
"tinymce": "6.8.5",
|
||||
"ts-jest": "29.1.5",
|
||||
"ts-jest": "29.3.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.4.5"
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
"@joplin/onenote-converter": "~3.4",
|
||||
"fs-extra": "11.2.0",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"sqlite3": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
@@ -345,8 +345,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
return {
|
||||
id: result.commandName,
|
||||
title: result.title,
|
||||
parent_id: null,
|
||||
fields: [],
|
||||
parent_id: null as string,
|
||||
fields: [] as string[],
|
||||
type: BaseModel.TYPE_COMMAND,
|
||||
};
|
||||
});
|
||||
|
@@ -51,10 +51,8 @@
|
||||
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('../../node_modules/@joplin/lib/node_modules/sqlite3/lib/sqlite3.js');
|
||||
return require('sqlite3');
|
||||
}
|
||||
|
||||
if (modulePath === 'fs-extra') {
|
||||
|
@@ -30,7 +30,7 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
// in the final bundle.
|
||||
name: 'joplin--relative-imports-for-externals',
|
||||
setup: build => {
|
||||
const externalRegex = /^(.*\.node|sqlite3|electron|@electron\/remote\/.*|electron\/.*|@mapbox\/node-pre-gyp|jsdom)$/;
|
||||
const externalRegex = /^(.*\.node|sqlite3|node-fetch|electron|@electron\/remote\/.*|electron\/.*|@mapbox\/node-pre-gyp|jsdom)$/;
|
||||
build.onResolve({ filter: externalRegex }, args => {
|
||||
// Electron packages don't need relative requires
|
||||
if (args.path === 'electron' || args.path.startsWith('electron/')) {
|
||||
|
@@ -25,7 +25,7 @@ async function main() {
|
||||
// wrong one. However it means it will have to be manually upgraded for each
|
||||
// new Electron release. Some ABI map there:
|
||||
// https://github.com/electron/node-abi/tree/master/test
|
||||
const forceAbiArgs = '--force-abi 134';
|
||||
const forceAbiArgs = '--force-abi 138';
|
||||
|
||||
if (isWindows()) {
|
||||
// Cannot run this in parallel, or the 64-bit version might end up
|
||||
|
3
packages/app-mobile/.gitignore
vendored
3
packages/app-mobile/.gitignore
vendored
@@ -67,7 +67,8 @@ yarn-error.log
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
components/**/*.bundle.js
|
||||
/**/*.bundle.js
|
||||
/**/*.bundle.css
|
||||
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 2097774
|
||||
versionName "3.4.1"
|
||||
versionCode 2097777
|
||||
versionName "3.4.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -6,6 +6,12 @@ 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() {
|
||||
|
||||
/**
|
||||
@@ -20,4 +26,25 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -540,6 +540,7 @@ const ComboBox: React.FC<Props> = ({
|
||||
};
|
||||
const activeId = `${baseId}-${selectedIndex}`;
|
||||
const searchResults = <NestableFlatList
|
||||
keyboardShouldPersistTaps="handled"
|
||||
ref={listRef}
|
||||
data={results}
|
||||
{...searchResultProps}
|
||||
|
@@ -63,7 +63,7 @@ const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) =
|
||||
/>
|
||||
<PromptButton
|
||||
buttonSpec={{
|
||||
text: _('Okay'),
|
||||
text: _('OK'),
|
||||
onPress: () => dialog.onSubmit(text),
|
||||
}}
|
||||
themeId={themeId}
|
||||
|
@@ -66,7 +66,6 @@ const DialogManager: React.FC<Props> = props => {
|
||||
const dialogComponents: React.ReactNode[] = [];
|
||||
for (const dialog of dialogModels) {
|
||||
const dialogProps = {
|
||||
key: dialog.key,
|
||||
containerStyle: styles.dialogContainer,
|
||||
themeId: props.themeId,
|
||||
};
|
||||
@@ -75,6 +74,7 @@ const DialogManager: React.FC<Props> = props => {
|
||||
<PromptDialog
|
||||
dialog={dialog}
|
||||
{...dialogProps}
|
||||
key={dialog.key}
|
||||
/>,
|
||||
);
|
||||
} else if (dialog.type === DialogType.TextInput) {
|
||||
@@ -82,6 +82,7 @@ const DialogManager: React.FC<Props> = props => {
|
||||
<TextInputDialog
|
||||
dialog={dialog}
|
||||
{...dialogProps}
|
||||
key={dialog.key}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
|
@@ -1,28 +1,31 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useMemo, useRef,
|
||||
forwardRef, Ref, useCallback, 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.
|
||||
// Use with caution -- don't load untrusted WebView HTML while testing.
|
||||
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(js: string) {
|
||||
return dom.window.eval(js);
|
||||
},
|
||||
injectJS: injectJs,
|
||||
postMessage(message: unknown) {
|
||||
const messageEventContent = {
|
||||
data: message,
|
||||
@@ -36,33 +39,61 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [dom]);
|
||||
}, [dom, injectJs]);
|
||||
|
||||
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;
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript + cssInjectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
// JSDOM polyfills
|
||||
dom.window.eval(`
|
||||
// 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]: () => {},
|
||||
};
|
||||
};
|
||||
window.scrollBy = (_amount) => { };
|
||||
|
||||
return range;
|
||||
// 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,
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
Object.defineProperty(HTMLIFrameElement.prototype, 'srcdoc', {
|
||||
set(value) {
|
||||
this.src = 'about:blank';
|
||||
setTimeout(() => {
|
||||
this.contentDocument.write(value);
|
||||
|
||||
polyfillIframeContentWindow(this.contentWindow);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
dom.window.eval(`
|
||||
@@ -77,10 +108,14 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
},
|
||||
});
|
||||
|
||||
dom.window.eval(injectedJavaScriptRef.current);
|
||||
// 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]);
|
||||
|
||||
|
||||
const onLoadEndRef = useRef(props.onLoadEnd);
|
||||
onLoadEndRef.current = props.onLoadEnd;
|
||||
const onLoadStartRef = useRef(props.onLoadStart);
|
||||
|
@@ -12,6 +12,7 @@ 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');
|
||||
|
||||
@@ -98,6 +99,9 @@ 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
|
||||
|
||||
@@ -131,7 +135,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
allowFileAccess={true}
|
||||
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
|
||||
webviewDebuggingEnabled={allowWebviewDebugging}
|
||||
injectedJavaScript={props.injectedJavaScript}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
onMessage={props.onMessage}
|
||||
onError={props.onError ?? onError}
|
||||
onLoadEnd={props.onLoadEnd}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useRef, useState,
|
||||
forwardRef, Ref, useCallback, 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');
|
||||
|
||||
@@ -20,24 +21,26 @@ 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(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,
|
||||
}, '*');
|
||||
},
|
||||
injectJS: injectJs,
|
||||
postMessage(message: unknown) {
|
||||
if (!iframeRef.current || !iframeRef.current.contentWindow) {
|
||||
logger.warn(`WebView(${props.webviewInstanceId}): Tried to post a message to an unloaded iframe.`);
|
||||
@@ -49,7 +52,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
}, '*');
|
||||
},
|
||||
};
|
||||
}, [props.webviewInstanceId]);
|
||||
}, [props.webviewInstanceId, injectJs]);
|
||||
|
||||
const [containerElement, setContainerElement] = useState<HTMLDivElement>();
|
||||
const containerRef = useRef(containerElement);
|
||||
@@ -62,9 +65,15 @@ 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(props.injectedJavaScript);
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript;
|
||||
const injectedJavaScriptRef = useRef(injectedJavaScript);
|
||||
injectedJavaScriptRef.current = injectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
const headHtml = `
|
||||
|
@@ -31,6 +31,7 @@ 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;
|
||||
|
@@ -0,0 +1,38 @@
|
||||
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,24 +1,20 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { useRef, useCallback } 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;
|
||||
@@ -69,27 +65,14 @@ function NoteBodyViewer(props: Props) {
|
||||
onResourceLongPress,
|
||||
});
|
||||
|
||||
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,
|
||||
const { api: renderer, pageSetup, webViewEventHandlers } = useWebViewSetup({
|
||||
webviewRef,
|
||||
onBodyScroll: onScroll,
|
||||
onPostMessage,
|
||||
setOnWebViewMessage,
|
||||
tempDir,
|
||||
pluginStates: props.pluginStates,
|
||||
themeId: props.themeId,
|
||||
});
|
||||
|
||||
const contentScripts = useContentScripts(props.pluginStates);
|
||||
|
||||
useRerenderHandler({
|
||||
renderer,
|
||||
fontSize: props.fontSize,
|
||||
@@ -102,16 +85,14 @@ function NoteBodyViewer(props: Props) {
|
||||
initialScroll: props.initialScroll,
|
||||
|
||||
paddingBottom: props.paddingBottom,
|
||||
|
||||
contentScripts,
|
||||
});
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
setWebViewLoaded(true);
|
||||
webViewEventHandlers.onLoadEnd();
|
||||
if (props.onLoadEnd) props.onLoadEnd();
|
||||
}, [props.onLoadEnd]);
|
||||
}, [props.onLoadEnd, webViewEventHandlers]);
|
||||
|
||||
const { html, injectedJs } = useSource(tempDir, props.themeId);
|
||||
const { html, js } = useSource(pageSetup, props.themeId);
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
@@ -121,10 +102,10 @@ function NoteBodyViewer(props: Props) {
|
||||
testID='NoteBodyViewer'
|
||||
html={html}
|
||||
allowFileAccessFromJs={true}
|
||||
injectedJavaScript={injectedJs}
|
||||
injectedJavaScript={js}
|
||||
mixedContentMode="always"
|
||||
onLoadEnd={onLoadEnd}
|
||||
onMessage={onWebViewMessage}
|
||||
onMessage={webViewEventHandlers.onMessage}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@@ -1,239 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
|
||||
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);
|
@@ -1,39 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
@@ -1,86 +0,0 @@
|
||||
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,26 +1,20 @@
|
||||
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 { useEffect, useRef, 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 { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
import { RendererControl, RenderOptions } from '../../../contentScripts/rendererBundle/types';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
|
||||
|
||||
export interface ResourceInfo {
|
||||
localState: unknown;
|
||||
localState: ResourceLocalStateEntity;
|
||||
item: ResourceEntity;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
renderer: Renderer;
|
||||
renderer: RendererControl;
|
||||
|
||||
noteBody: string;
|
||||
noteMarkupLanguage: MarkupLanguage;
|
||||
@@ -33,8 +27,6 @@ interface Props {
|
||||
initialScroll: number|undefined;
|
||||
|
||||
paddingBottom: number;
|
||||
|
||||
contentScripts: ExtraContentScriptSource[];
|
||||
}
|
||||
|
||||
const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
@@ -56,10 +48,35 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
|
||||
const logger = Logger.create('useRerenderHandler');
|
||||
|
||||
const useRerenderHandler = (props: Props) => {
|
||||
const { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss } = useEditPopup(props.themeId);
|
||||
const useResourceLoadCounter = (noteResources: Record<string, ResourceInfo>) => {
|
||||
const [lastResourceLoadCounter, setLastResourceLoadCounter] = useState(0);
|
||||
const [pluginSettingKeys, setPluginSettingKeys] = useState<Record<string, boolean>>({});
|
||||
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]);
|
||||
|
||||
// To address https://github.com/laurent22/joplin/issues/433
|
||||
//
|
||||
@@ -82,8 +99,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, lastResourceLoadCounter,
|
||||
createEditPopupSyntax, destroyEditPopupSyntax, pluginSettingKeys, props.fontSize,
|
||||
props.noteHash, props.noteResources, props.themeId, props.paddingBottom, resourceDownloadRerenderCounter,
|
||||
props.fontSize,
|
||||
];
|
||||
const previousDeps = usePrevious(effectDependencies, []);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -99,125 +116,43 @@ const useRerenderHandler = (props: Props) => {
|
||||
const previousHash = usePrevious(props.noteHash, '');
|
||||
const hashChanged = previousHash !== props.noteHash;
|
||||
|
||||
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 => {
|
||||
useAsyncEffect(async (event) => {
|
||||
if (onlyNoteBodyHasChanged && onlyCheckboxesHaveChanged) {
|
||||
logger.info('Only a checkbox has changed - not updating HTML');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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({
|
||||
const config: RenderOptions = {
|
||||
themeId: props.themeId,
|
||||
themeOverrides: {
|
||||
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.rerender({
|
||||
await props.renderer.rerenderToBody({
|
||||
language: props.noteMarkupLanguage,
|
||||
markup: props.noteBody,
|
||||
}, config);
|
||||
}, config, event);
|
||||
|
||||
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;
|
||||
|
@@ -1,49 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { RendererWebViewOptions } from '../bundledJs/types';
|
||||
import { themeStyle } from '../../global-style';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const useSource = (tempDirPath: string, themeId: number) => {
|
||||
const injectedJs = useMemo(() => {
|
||||
const subValues = Setting.subValues('markdown.plugin', Setting.toPlainObject());
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const pluginOptions: any = {};
|
||||
for (const n in subValues) {
|
||||
pluginOptions[n] = { enabled: subValues[n] };
|
||||
}
|
||||
|
||||
const rendererWebViewOptions: RendererWebViewOptions = {
|
||||
settings: {
|
||||
safeMode: Setting.value('isSafeMode'),
|
||||
tempDir: tempDirPath,
|
||||
resourceDir: Setting.value('resourceDir'),
|
||||
resourceDownloadMode: Setting.value('sync.resourceDownloadMode'),
|
||||
},
|
||||
// Web needs files to be transferred manually, since image SRCs can't reference
|
||||
// the Origin Private File System.
|
||||
useTransferredFiles: Platform.OS === 'web',
|
||||
pluginOptions,
|
||||
};
|
||||
|
||||
return `
|
||||
window.rendererWebViewOptions = ${JSON.stringify(rendererWebViewOptions)};
|
||||
|
||||
if (!window.injectedJsLoaded) {
|
||||
window.injectedJsLoaded = true;
|
||||
|
||||
${shim.injectedJs('webviewLib')}
|
||||
${shim.injectedJs('noteBodyViewerBundle')}
|
||||
}
|
||||
`;
|
||||
}, [tempDirPath]);
|
||||
import { PageSetupSources } from '../../../contentScripts/types';
|
||||
|
||||
const useSource = (rendererSource: PageSetupSources, themeId: number) => {
|
||||
const [paddingLeft, paddingRight] = useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return [theme.marginLeft, theme.marginRight];
|
||||
}, [themeId]);
|
||||
|
||||
const rendererBaseCss = rendererSource.css;
|
||||
const html = useMemo(() => {
|
||||
// iOS doesn't automatically adjust the WebView's font size to match users'
|
||||
// accessibility settings. To do this, we need to tell it to match the system font.
|
||||
@@ -75,6 +41,7 @@ const useSource = (tempDirPath: string, themeId: number) => {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
${defaultCss}
|
||||
${rendererBaseCss}
|
||||
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
|
||||
</style>
|
||||
</head>
|
||||
@@ -84,9 +51,9 @@ const useSource = (tempDirPath: string, themeId: number) => {
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}, [paddingLeft, paddingRight]);
|
||||
}, [paddingLeft, paddingRight, rendererBaseCss]);
|
||||
|
||||
return { html, injectedJs };
|
||||
return { html, js: rendererSource.js };
|
||||
};
|
||||
|
||||
export default useSource;
|
||||
|
@@ -1,91 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
// This contains the CodeMirror instance, which needs to be built into a bundle
|
||||
// using `yarn buildInjectedJs`. This bundle is then loaded from
|
||||
// NoteEditor.tsx into the webview.
|
||||
//
|
||||
// In general, since this file is harder to debug due to the intermediate built
|
||||
// step, it's better to keep it as light as possible - it should just be a light
|
||||
// wrapper to access CodeMirror functionalities. Anything else should be done
|
||||
// from NoteEditor.tsx.
|
||||
|
||||
import { EditorSettings } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger';
|
||||
import { WebViewToEditorApi } from '../types';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
|
||||
let loggerCreated = false;
|
||||
export const setUpLogger = () => {
|
||||
if (!loggerCreated) {
|
||||
const logger = new Logger();
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
loggerCreated = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const initCodeMirror = (
|
||||
parentElement: HTMLElement,
|
||||
initialText: string,
|
||||
initialNoteId: string,
|
||||
settings: EditorSettings,
|
||||
): CodeMirrorControl => {
|
||||
const messenger = new WebViewToRNMessenger<CodeMirrorControl, WebViewToEditorApi>('editor', null);
|
||||
|
||||
const control = createEditor(parentElement, {
|
||||
initialText,
|
||||
initialNoteId,
|
||||
settings,
|
||||
|
||||
onPasteFile: async (data) => {
|
||||
const reader = new FileReader();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
reader.onload = async () => {
|
||||
const dataUrl = reader.result as string;
|
||||
const base64 = dataUrl.replace(/^data:.*;base64,/, '');
|
||||
await messenger.remoteApi.onPasteFile(data.type, base64);
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to load file.'));
|
||||
|
||||
reader.readAsDataURL(data);
|
||||
});
|
||||
},
|
||||
|
||||
onLogMessage: message => {
|
||||
void messenger.remoteApi.logMessage(message);
|
||||
},
|
||||
onEvent: (event): void => {
|
||||
void messenger.remoteApi.onEditorEvent(event);
|
||||
},
|
||||
});
|
||||
|
||||
// Works around https://github.com/laurent22/joplin/issues/10047 by handling
|
||||
// the text/uri-list MIME type when pasting, rather than sending the paste event
|
||||
// to CodeMirror.
|
||||
//
|
||||
// TODO: Remove this workaround when the issue has been fixed upstream.
|
||||
control.on('paste', (_editor, event: ClipboardEvent) => {
|
||||
const clipboardData = event.clipboardData;
|
||||
if (clipboardData.types.length === 1 && clipboardData.types[0] === 'text/uri-list') {
|
||||
event.preventDefault();
|
||||
control.insertText(clipboardData.getData('text/uri-list'));
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Just adding an onclick listener seems sufficient to focus the editor when its background
|
||||
// is tapped.
|
||||
parentElement.addEventListener('click', (event) => {
|
||||
const activeElement = document.querySelector(':focus');
|
||||
if (!parentElement.contains(activeElement) && event.target === parentElement) {
|
||||
focus('initial editor focus', control);
|
||||
}
|
||||
});
|
||||
|
||||
messenger.setLocalInterface(control);
|
||||
return control;
|
||||
};
|
@@ -1,60 +0,0 @@
|
||||
<!--
|
||||
Open this file in a web browser to more easily debug the CodeMirror editor.
|
||||
Messages will show up in the console when posted.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<meta charset="utf-8"/>
|
||||
<title>CodeMirror test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="CodeMirror"></div>
|
||||
<script>
|
||||
// Override the default postMessage — codeMirrorBundle expects
|
||||
// this to be present.
|
||||
window.ReactNativeWebView = {
|
||||
postMessage: message => {
|
||||
console.log('postMessage:', message);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="./CodeMirror.bundle.js"></script>
|
||||
<script>
|
||||
const parent = document.querySelector('.CodeMirror');
|
||||
const initialText = 'Testing...';
|
||||
|
||||
const settings = {
|
||||
themeData: {
|
||||
fontSize: 12, // px
|
||||
fontFamily: 'serif',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
backgroundColor2: '#330',
|
||||
color2: '#ff0',
|
||||
backgroundColor3: '#404',
|
||||
color3: '#f0f',
|
||||
backgroundColor4: '#555',
|
||||
color4: '#0ff',
|
||||
appearance: 'dark',
|
||||
},
|
||||
themeId: 0,
|
||||
spellcheckEnabled: true,
|
||||
language: 'markdown',
|
||||
katexEnabled: true,
|
||||
useExternalSearch: false,
|
||||
readOnly: false,
|
||||
|
||||
keymap: 'default',
|
||||
|
||||
automatchBraces: false,
|
||||
ignoreModifiers: false,
|
||||
|
||||
indentWithTabs: false,
|
||||
};
|
||||
|
||||
window.cm = codeMirrorBundle.initCodeMirror(parent, initialText, settings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,20 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import ExtendedWebView from '../../ExtendedWebView';
|
||||
import { OnMessageEvent, WebViewControl } from '../../ExtendedWebView/types';
|
||||
import { clearAutosave } from './autosave';
|
||||
import { LocalizedStrings } from './js-draw/types';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { DialogContext } from '../../DialogManager';
|
||||
import useEditorMessenger from './utils/useEditorMessenger';
|
||||
import BackButtonService from '../../../services/BackButtonService';
|
||||
|
||||
import useWebViewSetup, { ImageEditorControl } from '../../../contentScripts/imageEditorBundle/useWebViewSetup';
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
@@ -28,69 +21,15 @@ interface Props {
|
||||
onExit: OnCancelCallback;
|
||||
}
|
||||
|
||||
const useCss = (editorTheme: Theme) => {
|
||||
return useMemo(() => {
|
||||
// Ensure we have contrast between the background and selection. Some themes
|
||||
// have the same backgroundColor and selectionColor2. (E.g. Aritim Dark)
|
||||
let selectionBackgroundColor = editorTheme.selectedColor2;
|
||||
if (selectionBackgroundColor === editorTheme.backgroundColor) {
|
||||
selectionBackgroundColor = editorTheme.selectedColor;
|
||||
}
|
||||
|
||||
return `
|
||||
:root .imageEditorContainer {
|
||||
--background-color-1: ${editorTheme.backgroundColor};
|
||||
--foreground-color-1: ${editorTheme.color};
|
||||
--background-color-2: ${editorTheme.backgroundColor3};
|
||||
--foreground-color-2: ${editorTheme.color3};
|
||||
--background-color-3: ${editorTheme.raisedBackgroundColor};
|
||||
--foreground-color-3: ${editorTheme.raisedColor};
|
||||
|
||||
--selection-background-color: ${editorTheme.backgroundColorHover3};
|
||||
--selection-foreground-color: ${editorTheme.color3};
|
||||
--primary-action-foreground-color: ${editorTheme.color4};
|
||||
|
||||
--primary-shadow-color: ${editorTheme.colorFaded};
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide the scrollbar. See scrollbar accessibility concerns
|
||||
(https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width#accessibility_concerns)
|
||||
for why this isn't done in js-draw itself. */
|
||||
.toolbar-tool-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Hide the save/close icons on small screens. This isn't done in the upstream
|
||||
js-draw repository partially because it isn't as well localized as Joplin
|
||||
(icons can be used to suggest the meaning of a button when a translation is
|
||||
unavailable). */
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--save .toolbar-icon,
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--exit .toolbar-icon {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}, [editorTheme]);
|
||||
};
|
||||
|
||||
const ImageEditor = (props: Props) => {
|
||||
const editorTheme: Theme = themeStyle(props.themeId);
|
||||
const webviewRef = useRef<WebViewControl|null>(null);
|
||||
const webViewRef = useRef<WebViewControl|null>(null);
|
||||
const [imageChanged, setImageChanged] = useState(false);
|
||||
|
||||
const editorControlRef = useRef<ImageEditorControl|null>(null);
|
||||
const dialogs = useContext(DialogContext);
|
||||
|
||||
const onRequestCloseEditor = useCallback((promptIfUnsaved: boolean) => {
|
||||
const onRequestCloseEditor = useCallback((promptIfUnsaved = true) => {
|
||||
const discardChangesAndClose = async () => {
|
||||
await clearAutosave();
|
||||
props.onExit();
|
||||
@@ -98,7 +37,7 @@ const ImageEditor = (props: Props) => {
|
||||
|
||||
if (!imageChanged || !promptIfUnsaved) {
|
||||
void discardChangesAndClose();
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
dialogs.prompt(
|
||||
@@ -113,13 +52,12 @@ const ImageEditor = (props: Props) => {
|
||||
onPress: () => {
|
||||
// saveDrawing calls props.onSave(...) which may close the
|
||||
// editor.
|
||||
webviewRef.current.injectJS('window.editorControl.saveThenExit()');
|
||||
void editorControlRef.current.saveThenExit();
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return true;
|
||||
}, [webviewRef, dialogs, props.onExit, imageChanged]);
|
||||
}, [dialogs, props.onExit, imageChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const hardwareBackPressListener = () => {
|
||||
@@ -133,9 +71,18 @@ const ImageEditor = (props: Props) => {
|
||||
};
|
||||
}, [onRequestCloseEditor]);
|
||||
|
||||
const css = useCss(editorTheme);
|
||||
const [html, setHtml] = useState('');
|
||||
const { pageSetup, api: editorControl, webViewEventHandlers } = useWebViewSetup({
|
||||
webViewRef,
|
||||
themeId: props.themeId,
|
||||
onSetImageChanged: setImageChanged,
|
||||
onAutoSave: writeAutosave,
|
||||
onSave: props.onSave,
|
||||
onRequestCloseEditor,
|
||||
resourceFilename: props.resourceFilename,
|
||||
});
|
||||
editorControlRef.current = editorControl;
|
||||
|
||||
const [html, setHtml] = useState('');
|
||||
useEffect(() => {
|
||||
setHtml(`
|
||||
<!DOCTYPE html>
|
||||
@@ -144,8 +91,8 @@ const ImageEditor = (props: Props) => {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
|
||||
<style id='main-style'>
|
||||
${css}
|
||||
<style>
|
||||
${pageSetup.css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
@@ -160,112 +107,12 @@ const ImageEditor = (props: Props) => {
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// A set of localization overrides (Joplin is better localized than js-draw).
|
||||
// All localizable strings (some unused?) can be found at
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/main/.github/ISSUE_TEMPLATE/translation-js-draw-new.yml
|
||||
const localizedStrings: LocalizedStrings = useMemo(() => ({
|
||||
save: _('Save'),
|
||||
close: _('Close'),
|
||||
undo: _('Undo'),
|
||||
redo: _('Redo'),
|
||||
}), []);
|
||||
|
||||
const appInfo = useMemo(() => {
|
||||
return {
|
||||
name: 'Joplin',
|
||||
description: `v${shim.appVersion()}`,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno,
|
||||
);
|
||||
};
|
||||
|
||||
window.onunhandledrejection = (error) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + error.reason,
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
if (window.editorControl === undefined) {
|
||||
${shim.injectedJs('svgEditorBundle')}
|
||||
|
||||
window.editorControl = svgEditorBundle.createJsDrawEditor(
|
||||
svgEditorBundle.createMessenger().remoteApi,
|
||||
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
|
||||
${JSON.stringify(Setting.value('locale'))},
|
||||
${JSON.stringify(localizedStrings)},
|
||||
${JSON.stringify({
|
||||
appInfo,
|
||||
...(shim.mobilePlatform() === 'web' ? {
|
||||
// Use the browser-default clipboard API on web.
|
||||
clipboardApi: null,
|
||||
} : {}),
|
||||
})},
|
||||
);
|
||||
}
|
||||
} catch(e) {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
'error: ' + e.message + ': ' + JSON.stringify(e)
|
||||
);
|
||||
}
|
||||
true;
|
||||
`, [localizedStrings, appInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
webviewRef.current?.injectJS(`
|
||||
document.querySelector('#main-style').textContent = ${JSON.stringify(css)};
|
||||
|
||||
if (window.editorControl) {
|
||||
window.editorControl.onThemeUpdate();
|
||||
}
|
||||
`);
|
||||
}, [css]);
|
||||
|
||||
const onReadyToLoadData = useCallback(async () => {
|
||||
const getInitialInjectedData = async () => {
|
||||
// On mobile, it's faster to load the image within the WebView with an XMLHttpRequest.
|
||||
// In this case, the image is loaded elsewhere.
|
||||
if (Platform.OS !== 'web') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// On web, however, this doesn't work, so the image needs to be loaded here.
|
||||
if (!props.resourceFilename) {
|
||||
return '';
|
||||
}
|
||||
return await shim.fsDriver().readFile(props.resourceFilename, 'utf-8');
|
||||
};
|
||||
// It can take some time for initialSVGData to be transferred to the WebView.
|
||||
// Thus, do so after the main content has been loaded.
|
||||
webviewRef.current.injectJS(`(async () => {
|
||||
if (window.editorControl) {
|
||||
const initialSVGPath = ${JSON.stringify(props.resourceFilename)};
|
||||
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
|
||||
const initialData = ${JSON.stringify(await getInitialInjectedData())};
|
||||
|
||||
editorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData, initialData);
|
||||
}
|
||||
})();`);
|
||||
}, [webviewRef, props.resourceFilename]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onError = useCallback((event: any) => {
|
||||
logger.error('ImageEditor: WebView error: ', event);
|
||||
}, []);
|
||||
|
||||
const messenger = useEditorMessenger({
|
||||
webviewRef,
|
||||
setImageChanged,
|
||||
onReadyToLoadData,
|
||||
onSave: props.onSave,
|
||||
onRequestCloseEditor,
|
||||
});
|
||||
|
||||
const onWebViewMessage = webViewEventHandlers.onMessage;
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
if (typeof data === 'string' && data.startsWith('error:')) {
|
||||
@@ -273,18 +120,18 @@ const ImageEditor = (props: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
messenger.onWebViewMessage(event);
|
||||
}, [messenger]);
|
||||
onWebViewMessage(event);
|
||||
}, [onWebViewMessage]);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
injectedJavaScript={pageSetup.js}
|
||||
allowFileAccessFromJs={true}
|
||||
onMessage={onMessage}
|
||||
onLoadEnd={messenger.onWebViewLoaded}
|
||||
onLoadEnd={webViewEventHandlers.onLoadEnd}
|
||||
onError={onError}
|
||||
ref={webviewRef}
|
||||
ref={webViewRef}
|
||||
webviewInstanceId={'image-editor-js-draw'}
|
||||
/>
|
||||
);
|
||||
|
@@ -1,11 +0,0 @@
|
||||
// .replaceChildren is not supported in Chromium 83, which is the default for Android 11
|
||||
// (unless auto-updated from the Google Play store).
|
||||
HTMLElement.prototype.replaceChildren ??= function(this: HTMLElement, ...nodes: Node[]) {
|
||||
while (this.children.length) {
|
||||
this.children[0].remove();
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
this.appendChild(node);
|
||||
}
|
||||
};
|
187
packages/app-mobile/components/NoteEditor/MarkdownEditor.tsx
Normal file
187
packages/app-mobile/components/NoteEditor/MarkdownEditor.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { NativeSyntheticEvent } from 'react-native';
|
||||
|
||||
import { EditorProps } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
import useWebViewSetup from '../../contentScripts/markdownEditorBundle/useWebViewSetup';
|
||||
|
||||
const logger = Logger.create('MarkdownEditor');
|
||||
|
||||
function useCss(themeId: number): string {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const themeVariableCss = themeToCss(theme);
|
||||
return `
|
||||
${themeVariableCss}
|
||||
|
||||
:root {
|
||||
background-color: ${theme.backgroundColor};
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
/* Prefer 100% -- 100vw shows an unnecessary horizontal scrollbar in Google Chrome (desktop). */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 10px;
|
||||
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(100, 100, 100, 0.7) rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
*::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
border: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 100, 100, 0.7);
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: unset;
|
||||
scrollbar-color: unset;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}, [themeId]);
|
||||
}
|
||||
|
||||
function useHtml(): string {
|
||||
return useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>${_('Note editor')}</title>
|
||||
<style>
|
||||
/* For better scrolling on iOS (working scrollbar) we use external, rather than internal,
|
||||
scrolling. */
|
||||
.cm-scroller {
|
||||
overflow: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="CodeMirror" style="height:100%;" autocapitalize="on"></div>
|
||||
</body>
|
||||
</html>
|
||||
`, []);
|
||||
}
|
||||
|
||||
const MarkdownEditor: React.FC<EditorProps> = props => {
|
||||
const webviewRef = props.webviewRef;
|
||||
|
||||
const editorWebViewSetup = useWebViewSetup({
|
||||
initialSelection: props.initialSelection,
|
||||
noteHash: props.noteHash,
|
||||
globalSearch: props.globalSearch,
|
||||
onEditorEvent: props.onEditorEvent,
|
||||
onAttachFile: props.onAttach,
|
||||
editorOptions: {
|
||||
parentElementOrClassName: 'CodeMirror',
|
||||
initialText: props.initialText,
|
||||
initialNoteId: props.noteId,
|
||||
settings: props.editorSettings,
|
||||
},
|
||||
webviewRef,
|
||||
pluginStates: props.plugins,
|
||||
});
|
||||
|
||||
props.editorRef.current = editorWebViewSetup.api.mainEditor;
|
||||
|
||||
const injectedJavaScript = `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
console.error(message);
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
window.onunhandledrejection = (event) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: Unhandled promise rejection: " + event
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
${editorWebViewSetup.pageSetup.js}
|
||||
} catch (e) {
|
||||
console.error('Setup error: ', e);
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
}
|
||||
|
||||
true;
|
||||
`;
|
||||
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml();
|
||||
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
if (typeof data === 'string' && data.indexOf('error:') === 0) {
|
||||
logger.error('CodeMirror error', data);
|
||||
return;
|
||||
}
|
||||
|
||||
editorWebViewSetup.webViewEventHandlers.onMessage(event);
|
||||
}, [editorWebViewSetup]);
|
||||
|
||||
const onError = useCallback((event: NativeSyntheticEvent<WebViewErrorEvent>) => {
|
||||
logger.error(`Load error: Code ${event.nativeEvent.code}: ${event.nativeEvent.description}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
ref={webviewRef}
|
||||
webviewInstanceId='MarkdownEditor'
|
||||
testID='MarkdownEditor'
|
||||
scrollEnabled={true}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
css={css}
|
||||
hasPluginScripts={editorWebViewSetup.hasPlugins}
|
||||
onMessage={onMessage}
|
||||
onLoadEnd={editorWebViewSetup.webViewEventHandlers.onLoadEnd}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
@@ -15,10 +15,31 @@ import mockCommandRuntimes from '../EditorToolbar/testing/mockCommandRuntimes';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { EditorType } from './types';
|
||||
|
||||
let store: Store<AppState>;
|
||||
let registeredRuntime: RegisteredRuntime;
|
||||
|
||||
const defaultEditorProps = {
|
||||
themeId: Setting.THEME_ARITIM_DARK,
|
||||
markupLanguage: MarkupLanguage.Markdown,
|
||||
initialText: 'Testing...',
|
||||
globalSearch: '',
|
||||
noteId: '',
|
||||
noteHash: '',
|
||||
style: {},
|
||||
toolbarEnabled: true,
|
||||
readOnly: false,
|
||||
onChange: ()=>{},
|
||||
onSelectionChange: ()=>{},
|
||||
onUndoRedoDepthChange: ()=>{},
|
||||
onAttach: async ()=>{},
|
||||
noteResources: {},
|
||||
plugins: {},
|
||||
mode: EditorType.Markdown,
|
||||
};
|
||||
|
||||
describe('NoteEditor', () => {
|
||||
beforeAll(() => {
|
||||
// This allows the NoteEditor test to register editor commands without errors.
|
||||
@@ -45,19 +66,8 @@ describe('NoteEditor', () => {
|
||||
const wrappedNoteEditor = render(
|
||||
<TestProviderStack store={store}>
|
||||
<NoteEditor
|
||||
themeId={Setting.THEME_ARITIM_DARK}
|
||||
initialText='Testing...'
|
||||
globalSearch=''
|
||||
noteId=''
|
||||
noteHash=''
|
||||
style={{}}
|
||||
toolbarEnabled={true}
|
||||
readOnly={false}
|
||||
onChange={()=>{}}
|
||||
onSelectionChange={()=>{}}
|
||||
onUndoRedoDepthChange={()=>{}}
|
||||
onAttach={async ()=>{}}
|
||||
plugins={{}}
|
||||
ref={undefined}
|
||||
{...defaultEditorProps}
|
||||
/>
|
||||
</TestProviderStack>,
|
||||
);
|
||||
@@ -99,4 +109,27 @@ describe('NoteEditor', () => {
|
||||
|
||||
wrappedNoteEditor.unmount();
|
||||
});
|
||||
|
||||
it('should show a warning banner the first time the Rich Text Editor is used', () => {
|
||||
const wrappedNoteEditor = render(
|
||||
<TestProviderStack store={store}>
|
||||
<NoteEditor
|
||||
ref={undefined}
|
||||
{...defaultEditorProps}
|
||||
mode={EditorType.RichText}
|
||||
/>
|
||||
</TestProviderStack>,
|
||||
);
|
||||
|
||||
const warningBannerQuery = /This Rich Text editor has a number of limitations.*/;
|
||||
const warning = screen.getByText(warningBannerQuery);
|
||||
expect(warning).toBeVisible();
|
||||
|
||||
// Pressing dismiss should dismiss the warning
|
||||
const dismissButton = screen.getByHintText('Hides warning');
|
||||
fireEvent.press(dismissButton);
|
||||
expect(screen.queryByText(warningBannerQuery)).toBeNull();
|
||||
|
||||
wrappedNoteEditor.unmount();
|
||||
});
|
||||
});
|
||||
|
@@ -1,46 +1,49 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import EditLinkDialog from './EditLinkDialog';
|
||||
import { defaultSearchState, SearchPanel } from './SearchPanel';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
import { WebViewControl } from '../ExtendedWebView/types';
|
||||
|
||||
import * as React from 'react';
|
||||
import { forwardRef, RefObject, useEffect, useImperativeHandle } from 'react';
|
||||
import { Ref, RefObject, useEffect, useImperativeHandle } from 'react';
|
||||
import { useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { LayoutChangeEvent, NativeSyntheticEvent, View, ViewStyle } from 'react-native';
|
||||
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
|
||||
import { editorFont } from '../global-style';
|
||||
|
||||
import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types';
|
||||
import { EditorControl, EditorSettings, SelectionRange, WebViewToEditorApi } from './types';
|
||||
import { EditorControl, EditorSettings, EditorType } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
|
||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||
import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins';
|
||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import useEditorCommandHandler from './hooks/useEditorCommandHandler';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
import { join, dirname } from 'path';
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import EditorToolbar from '../EditorToolbar/EditorToolbar';
|
||||
import { SelectionRange } from '../../contentScripts/markdownEditorBundle/types';
|
||||
import MarkdownEditor from './MarkdownEditor';
|
||||
import RichTextEditor from './RichTextEditor';
|
||||
import { ResourceInfos } from '@joplin/renderer/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { join } from 'path';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { dirname } from '@joplin/utils/path';
|
||||
import { toFileExtension } from '@joplin/lib/mime-utils';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import WarningBanner from './WarningBanner';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
|
||||
type OnAttachCallback = (filePath?: string)=> Promise<void>;
|
||||
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
interface Props {
|
||||
ref: Ref<EditorControl>;
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
mode: EditorType;
|
||||
markupLanguage: MarkupLanguage;
|
||||
noteId: string;
|
||||
noteHash: string;
|
||||
globalSearch: string;
|
||||
@@ -49,6 +52,7 @@ interface Props {
|
||||
toolbarEnabled: boolean;
|
||||
readOnly: boolean;
|
||||
plugins: PluginStates;
|
||||
noteResources: ResourceInfos;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
@@ -61,103 +65,6 @@ function fontFamilyFromSettings() {
|
||||
return font ? `${font}, sans-serif` : 'sans-serif';
|
||||
}
|
||||
|
||||
function useCss(themeId: number): string {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const themeVariableCss = themeToCss(theme);
|
||||
return `
|
||||
${themeVariableCss}
|
||||
|
||||
:root {
|
||||
background-color: ${theme.backgroundColor};
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
/* Prefer 100% -- 100vw shows an unnecessary horizontal scrollbar in Google Chrome (desktop). */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 10px;
|
||||
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(100, 100, 100, 0.7) rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
*::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
border: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 100, 100, 0.7);
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: unset;
|
||||
scrollbar-color: unset;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}, [themeId]);
|
||||
}
|
||||
|
||||
const themeStyleSheetClassName = 'note-editor-styles';
|
||||
function useHtml(initialCss: string): string {
|
||||
const cssRef = useRef(initialCss);
|
||||
cssRef.current = initialCss;
|
||||
|
||||
return useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>${_('Note editor')}</title>
|
||||
<style>
|
||||
/* For better scrolling on iOS (working scrollbar) we use external, rather than internal,
|
||||
scrolling. */
|
||||
.cm-scroller {
|
||||
overflow: none;
|
||||
}
|
||||
</style>
|
||||
<style class=${JSON.stringify(themeStyleSheetClassName)}>
|
||||
${cssRef.current}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="CodeMirror" style="height:100%;" autocapitalize="on"></div>
|
||||
</body>
|
||||
</html>
|
||||
`, []);
|
||||
}
|
||||
|
||||
function editorTheme(themeId: number) {
|
||||
const fontSizeInPx = Setting.value('style.editor.fontSize');
|
||||
|
||||
@@ -167,6 +74,7 @@ function editorTheme(themeId: number) {
|
||||
const estimatedFontSizeInEm = fontSizeInPx / 16;
|
||||
|
||||
return {
|
||||
themeId,
|
||||
...themeStyle(themeId),
|
||||
|
||||
// To allow accessibility font scaling, we also need to set the
|
||||
@@ -181,54 +89,54 @@ function editorTheme(themeId: number) {
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
bodyControl: EditorBodyControl,
|
||||
editorRef: RefObject<EditorBodyControl>,
|
||||
webviewRef: RefObject<WebViewControl>,
|
||||
setLinkDialogVisible: OnSetVisibleCallback,
|
||||
setSearchState: OnSearchStateChangeCallback,
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
const execEditorCommand = (command: EditorCommandType) => {
|
||||
void bodyControl.execCommand(command);
|
||||
void editorRef.current.execCommand(command);
|
||||
};
|
||||
|
||||
const setSearchStateCallback = (state: SearchState) => {
|
||||
bodyControl.setSearchState(state);
|
||||
editorRef.current.setSearchState(state);
|
||||
setSearchState(state);
|
||||
};
|
||||
|
||||
const control: EditorControl = {
|
||||
supportsCommand(command: EditorCommandType) {
|
||||
return bodyControl.supportsCommand(command);
|
||||
return editorRef.current.supportsCommand(command);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
execCommand(command, ...args: any[]) {
|
||||
return bodyControl.execCommand(command, ...args);
|
||||
return editorRef.current.execCommand(command, ...args);
|
||||
},
|
||||
|
||||
focus() {
|
||||
void bodyControl.execCommand(EditorCommandType.Focus);
|
||||
void editorRef.current.execCommand(EditorCommandType.Focus);
|
||||
},
|
||||
|
||||
undo() {
|
||||
bodyControl.undo();
|
||||
editorRef.current.undo();
|
||||
},
|
||||
redo() {
|
||||
bodyControl.redo();
|
||||
editorRef.current.redo();
|
||||
},
|
||||
select(anchor: number, head: number) {
|
||||
bodyControl.select(anchor, head);
|
||||
editorRef.current.select(anchor, head);
|
||||
},
|
||||
setScrollPercent(fraction: number) {
|
||||
bodyControl.setScrollPercent(fraction);
|
||||
editorRef.current.setScrollPercent(fraction);
|
||||
},
|
||||
insertText(text: string) {
|
||||
bodyControl.insertText(text);
|
||||
editorRef.current.insertText(text);
|
||||
},
|
||||
updateBody(newBody: string) {
|
||||
bodyControl.updateBody(newBody);
|
||||
editorRef.current.updateBody(newBody);
|
||||
},
|
||||
updateSettings(newSettings: EditorSettings) {
|
||||
bodyControl.updateSettings(newSettings);
|
||||
editorRef.current.updateSettings(newSettings);
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
@@ -276,7 +184,7 @@ const useEditorControl = (
|
||||
execEditorCommand(EditorCommandType.IndentLess);
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
bodyControl.updateLink(label, url);
|
||||
editorRef.current.updateLink(label, url);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
execEditorCommand(EditorCommandType.ScrollSelectionIntoView);
|
||||
@@ -292,7 +200,7 @@ const useEditorControl = (
|
||||
},
|
||||
|
||||
setContentScripts: async (plugins: ContentScriptData[]) => {
|
||||
return bodyControl.setContentScripts(plugins);
|
||||
return editorRef.current.setContentScripts(plugins);
|
||||
},
|
||||
|
||||
setSearchState: setSearchStateCallback,
|
||||
@@ -320,37 +228,31 @@ const useEditorControl = (
|
||||
|
||||
setSearchState: setSearchStateCallback,
|
||||
},
|
||||
|
||||
onResourceDownloaded: (id: string) => {
|
||||
editorRef.current.onResourceDownloaded(id);
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
editorRef.current.remove();
|
||||
},
|
||||
};
|
||||
|
||||
return control;
|
||||
}, [webviewRef, bodyControl, setLinkDialogVisible, setSearchState]);
|
||||
}, [webviewRef, editorRef, setLinkDialogVisible, setSearchState]);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
function NoteEditor(props: Props) {
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
const setInitialSelectionJs = props.initialSelection ? `
|
||||
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
|
||||
cm.execCommand('scrollSelectionIntoView');
|
||||
` : '';
|
||||
const jumpToHashJs = props.noteHash ? `
|
||||
cm.jumpToHash(${JSON.stringify(props.noteHash)});
|
||||
` : '';
|
||||
const setInitialSearchJs = props.globalSearch ? `
|
||||
cm.setSearchState(${JSON.stringify({
|
||||
...defaultSearchState,
|
||||
searchText: props.globalSearch,
|
||||
})})
|
||||
` : '';
|
||||
|
||||
const editorSettings: EditorSettings = useMemo(() => ({
|
||||
themeId: props.themeId,
|
||||
themeData: editorTheme(props.themeId),
|
||||
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
|
||||
language: EditorLanguageType.Markdown,
|
||||
inlineRenderingEnabled: Setting.value('editor.inlineRendering'),
|
||||
imageRenderingEnabled: Setting.value('editor.imageRendering'),
|
||||
language: props.markupLanguage === MarkupLanguage.Html ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
useExternalSearch: true,
|
||||
readOnly: props.readOnly,
|
||||
|
||||
@@ -365,208 +267,84 @@ function NoteEditor(props: Props, ref: any) {
|
||||
indentWithTabs: true,
|
||||
|
||||
editorLabel: _('Markdown editor'),
|
||||
}), [props.themeId, props.readOnly]);
|
||||
}), [props.themeId, props.readOnly, props.markupLanguage]);
|
||||
|
||||
const injectedJavaScript = `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
window.onunhandledrejection = (event) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: Unhandled promise rejection: " + event
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.cm) {
|
||||
// This variable is not used within this script
|
||||
// but is called using "injectJavaScript" from
|
||||
// the wrapper component.
|
||||
window.cm = null;
|
||||
|
||||
try {
|
||||
${shim.injectedJs('codeMirrorBundle')};
|
||||
codeMirrorBundle.setUpLogger();
|
||||
|
||||
const parentElement = document.getElementsByClassName('CodeMirror')[0];
|
||||
// On Android, injectJavaScript is run twice -- once before the parent element exists.
|
||||
// To avoid logging unnecessary errors to the console, skip setup in this case:
|
||||
if (parentElement) {
|
||||
const initialText = ${JSON.stringify(props.initialText)};
|
||||
const settings = ${JSON.stringify(editorSettings)};
|
||||
|
||||
window.cm = codeMirrorBundle.initCodeMirror(
|
||||
parentElement,
|
||||
initialText,
|
||||
${JSON.stringify(props.noteId)},
|
||||
settings
|
||||
);
|
||||
|
||||
${jumpToHashJs}
|
||||
// Set the initial selection after jumping to the header -- the initial selection,
|
||||
// if specified, should take precedence.
|
||||
${setInitialSelectionJs}
|
||||
${setInitialSearchJs}
|
||||
|
||||
window.onresize = () => {
|
||||
cm.execCommand('scrollSelectionIntoView');
|
||||
};
|
||||
} else {
|
||||
console.warn('No parent element for the editor found. This may mean that the editor HTML is still loading.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
true;
|
||||
`;
|
||||
|
||||
const css = useCss(props.themeId);
|
||||
|
||||
useEffect(() => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.injectJS(`
|
||||
const styleClass = ${JSON.stringify(themeStyleSheetClassName)};
|
||||
for (const oldStyle of [...document.getElementsByClassName(styleClass)]) {
|
||||
oldStyle.remove();
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.classList.add(styleClass);
|
||||
|
||||
style.appendChild(document.createTextNode(${JSON.stringify(css)}));
|
||||
document.head.appendChild(style);
|
||||
`);
|
||||
}
|
||||
}, [css]);
|
||||
|
||||
// Scroll to the new hash, if it changes.
|
||||
const isFirstScrollRef = useRef(true);
|
||||
useEffect(() => {
|
||||
// The first "jump to header" is handled during editor setup and shouldn't
|
||||
// be handled a second time:
|
||||
if (isFirstScrollRef.current) {
|
||||
isFirstScrollRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (jumpToHashJs && webviewRef.current) {
|
||||
webviewRef.current.injectJS(jumpToHashJs);
|
||||
}
|
||||
}, [jumpToHashJs]);
|
||||
|
||||
const html = useHtml(css);
|
||||
const [selectionState, setSelectionState] = useState<SelectionFormatting>(defaultSelectionFormatting);
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
const onEditorEvent = useRef((_event: EditorEvent) => {});
|
||||
const editorControlRef = useRef<EditorControl|null>(null);
|
||||
const onEditorEvent = (event: EditorEvent) => {
|
||||
let exhaustivenessCheck: never;
|
||||
switch (event.kind) {
|
||||
case EditorEventType.Change:
|
||||
props.onChange(event);
|
||||
break;
|
||||
case EditorEventType.UndoRedoDepthChange:
|
||||
props.onUndoRedoDepthChange(event);
|
||||
break;
|
||||
case EditorEventType.SelectionRangeChange:
|
||||
props.onSelectionChange(event);
|
||||
break;
|
||||
case EditorEventType.SelectionFormattingChange:
|
||||
setSelectionState(event.formatting);
|
||||
break;
|
||||
case EditorEventType.EditLink:
|
||||
editorControl.showLinkDialog();
|
||||
break;
|
||||
case EditorEventType.FollowLink:
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
break;
|
||||
case EditorEventType.UpdateSearchDialog:
|
||||
setSearchState(event.searchState);
|
||||
|
||||
const onAttachRef = useRef(props.onAttach);
|
||||
onAttachRef.current = props.onAttach;
|
||||
|
||||
const editorMessenger = useMemo(() => {
|
||||
const localApi: WebViewToEditorApi = {
|
||||
async onEditorEvent(event) {
|
||||
onEditorEvent.current(event);
|
||||
},
|
||||
async logMessage(message) {
|
||||
logger.debug('CodeMirror:', message);
|
||||
},
|
||||
async onPasteFile(type, data) {
|
||||
const tempFilePath = join(Setting.value('tempDir'), `paste.${uuid.createNano()}.${mimeUtils.toFileExtension(type)}`);
|
||||
await shim.fsDriver().mkdir(dirname(tempFilePath));
|
||||
try {
|
||||
await shim.fsDriver().writeFile(tempFilePath, data, 'base64');
|
||||
await onAttachRef.current(tempFilePath);
|
||||
} finally {
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
}
|
||||
},
|
||||
};
|
||||
const messenger = new RNToWebViewMessenger<WebViewToEditorApi, EditorBodyControl>(
|
||||
'editor', webviewRef, localApi,
|
||||
);
|
||||
return messenger;
|
||||
}, []);
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
break;
|
||||
case EditorEventType.Remove:
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
break;
|
||||
default:
|
||||
exhaustivenessCheck = event;
|
||||
return exhaustivenessCheck;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const editorRef = useRef<EditorBodyControl|null>(null);
|
||||
const editorControl = useEditorControl(
|
||||
editorMessenger.remoteApi, webviewRef, setLinkDialogVisible, setSearchState,
|
||||
editorRef, webviewRef, setLinkDialogVisible, setSearchState,
|
||||
);
|
||||
editorControlRef.current = editorControl;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
editorControl.updateSettings(editorSettings);
|
||||
}, [editorSettings, editorControl]);
|
||||
|
||||
const lastNoteResources = useRef<ResourceInfos>(props.noteResources);
|
||||
useEffect(() => {
|
||||
const isDownloaded = (resourceInfos: ResourceInfos, resourceId: string) => {
|
||||
return resourceInfos[resourceId]?.localState?.fetch_status === Resource.FETCH_STATUS_DONE;
|
||||
};
|
||||
for (const key in props.noteResources) {
|
||||
const wasDownloaded = isDownloaded(lastNoteResources.current, key);
|
||||
if (!wasDownloaded && isDownloaded(props.noteResources, key)) {
|
||||
editorControl.onResourceDownloaded(key);
|
||||
}
|
||||
}
|
||||
}, [props.noteResources, editorControl]);
|
||||
|
||||
useEditorCommandHandler(editorControl);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
useImperativeHandle(props.ref, () => {
|
||||
return editorControl;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onEditorEvent.current = (event: EditorEvent) => {
|
||||
let exhaustivenessCheck: never;
|
||||
switch (event.kind) {
|
||||
case EditorEventType.Change:
|
||||
props.onChange(event);
|
||||
break;
|
||||
case EditorEventType.UndoRedoDepthChange:
|
||||
props.onUndoRedoDepthChange(event);
|
||||
break;
|
||||
case EditorEventType.SelectionRangeChange:
|
||||
props.onSelectionChange(event);
|
||||
break;
|
||||
case EditorEventType.SelectionFormattingChange:
|
||||
setSelectionState(event.formatting);
|
||||
break;
|
||||
case EditorEventType.EditLink:
|
||||
editorControl.showLinkDialog();
|
||||
break;
|
||||
case EditorEventType.UpdateSearchDialog:
|
||||
setSearchState(event.searchState);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
break;
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
break;
|
||||
default:
|
||||
exhaustivenessCheck = event;
|
||||
return exhaustivenessCheck;
|
||||
}
|
||||
return;
|
||||
};
|
||||
}, [props.onChange, props.onUndoRedoDepthChange, props.onSelectionChange, editorControl]);
|
||||
|
||||
const codeMirrorPlugins = useCodeMirrorPlugins(props.plugins);
|
||||
useEffect(() => {
|
||||
void editorControl.setContentScripts(codeMirrorPlugins);
|
||||
}, [codeMirrorPlugins, editorControl]);
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
editorMessenger.onWebViewLoaded();
|
||||
}, [editorMessenger]);
|
||||
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
if (typeof data === 'string' && data.indexOf('error:') === 0) {
|
||||
logger.error('CodeMirror error', data);
|
||||
return;
|
||||
}
|
||||
|
||||
editorMessenger.onWebViewMessage(event);
|
||||
}, [editorMessenger]);
|
||||
|
||||
const onError = useCallback((event: NativeSyntheticEvent<WebViewErrorEvent>) => {
|
||||
logger.error(`Load error: Code ${event.nativeEvent.code}: ${event.nativeEvent.description}`);
|
||||
}, []);
|
||||
|
||||
const [hasSpaceForToolbar, setHasSpaceForToolbar] = useState(true);
|
||||
const toolbarEnabled = props.toolbarEnabled && hasSpaceForToolbar;
|
||||
|
||||
@@ -580,12 +358,24 @@ function NoteEditor(props: Props, ref: any) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onAttach = useCallback(async (type: string, base64: string) => {
|
||||
const tempFilePath = join(Setting.value('tempDir'), `paste.${uuid.createNano()}.${toFileExtension(type)}`);
|
||||
await shim.fsDriver().mkdir(dirname(tempFilePath));
|
||||
try {
|
||||
await shim.fsDriver().writeFile(tempFilePath, base64, 'base64');
|
||||
await props.onAttach(tempFilePath);
|
||||
} finally {
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
}
|
||||
}, [props.onAttach]);
|
||||
|
||||
const toolbarEditorState = useMemo(() => ({
|
||||
selectionState,
|
||||
searchVisible: searchState.dialogVisible,
|
||||
}), [selectionState, searchState.dialogVisible]);
|
||||
|
||||
const toolbar = <EditorToolbar editorState={toolbarEditorState} />;
|
||||
const EditorComponent = props.mode === EditorType.Markdown ? MarkdownEditor : RichTextEditor;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -607,20 +397,25 @@ function NoteEditor(props: Props, ref: any) {
|
||||
flexShrink: 0,
|
||||
minHeight: '30%',
|
||||
}}>
|
||||
<ExtendedWebView
|
||||
webviewInstanceId='NoteEditor'
|
||||
testID='NoteEditor'
|
||||
scrollEnabled={true}
|
||||
ref={webviewRef}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
hasPluginScripts={codeMirrorPlugins.length > 0}
|
||||
onMessage={onMessage}
|
||||
onLoadEnd={onLoadEnd}
|
||||
onError={onError}
|
||||
<EditorComponent
|
||||
editorRef={editorRef}
|
||||
webviewRef={webviewRef}
|
||||
themeId={props.themeId}
|
||||
noteId={props.noteId}
|
||||
noteHash={props.noteHash}
|
||||
initialText={props.initialText}
|
||||
initialSelection={props.initialSelection}
|
||||
editorSettings={editorSettings}
|
||||
globalSearch={props.globalSearch}
|
||||
onEditorEvent={onEditorEvent}
|
||||
noteResources={props.noteResources}
|
||||
plugins={props.plugins}
|
||||
onAttach={onAttach}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<WarningBanner editorType={props.mode}/>
|
||||
|
||||
<SearchPanel
|
||||
editorSettings={editorSettings}
|
||||
searchControl={editorControl.searchControl}
|
||||
@@ -632,4 +427,4 @@ function NoteEditor(props: Props, ref: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(NoteEditor);
|
||||
export default NoteEditor;
|
||||
|
@@ -0,0 +1,437 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
||||
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { createNoteAndResource, resourceFetcher, setupDatabaseAndSynchronizer, supportDir, switchClient, synchronizerStart } from '@joplin/lib/testing/test-utils';
|
||||
import getWebViewWindowById from '../../utils/testing/getWebViewWindowById';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import RichTextEditor from './RichTextEditor';
|
||||
import createTestEditorProps from './testing/createTestEditorProps';
|
||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||
import { RefObject, useCallback, useMemo } from 'react';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { ResourceInfos } from '@joplin/renderer/types';
|
||||
import { EditorControl, EditorLanguageType } from '@joplin/editor/types';
|
||||
import attachedResources from '@joplin/lib/utils/attachedResources';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorSettings } from './types';
|
||||
import { pregQuote } from '@joplin/lib/string-utils';
|
||||
|
||||
|
||||
interface WrapperProps {
|
||||
ref?: RefObject<EditorControl>;
|
||||
noteResources?: ResourceInfos;
|
||||
onBodyChange: (newBody: string)=> void;
|
||||
onLinkClick?: (link: string)=> void;
|
||||
note?: NoteEntity;
|
||||
noteBody: string;
|
||||
}
|
||||
|
||||
const defaultEditorProps = createTestEditorProps();
|
||||
const testStore = createMockReduxStore();
|
||||
const WrappedEditor: React.FC<WrapperProps> = (
|
||||
{
|
||||
noteBody,
|
||||
note,
|
||||
onBodyChange,
|
||||
onLinkClick,
|
||||
noteResources,
|
||||
ref,
|
||||
}: WrapperProps,
|
||||
) => {
|
||||
const onEvent = useCallback((event: EditorEvent) => {
|
||||
if (event.kind === EditorEventType.Change) {
|
||||
onBodyChange(event.value);
|
||||
} else if (event.kind === EditorEventType.FollowLink) {
|
||||
if (!onLinkClick) {
|
||||
throw new Error('No mock function for onLinkClick registered.');
|
||||
}
|
||||
|
||||
onLinkClick(event.link);
|
||||
}
|
||||
}, [onBodyChange, onLinkClick]);
|
||||
|
||||
const editorSettings = useMemo((): EditorSettings => {
|
||||
const isHtml = note?.markup_language === MarkupLanguage.Html;
|
||||
return {
|
||||
...defaultEditorProps.editorSettings,
|
||||
language: isHtml ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
};
|
||||
}, [note]);
|
||||
|
||||
return <TestProviderStack store={testStore}>
|
||||
<RichTextEditor
|
||||
{...defaultEditorProps}
|
||||
editorSettings={editorSettings}
|
||||
onEditorEvent={onEvent}
|
||||
initialText={noteBody}
|
||||
noteId={note?.id ?? defaultEditorProps.noteId}
|
||||
noteResources={noteResources ?? defaultEditorProps.noteResources}
|
||||
editorRef={ref ?? defaultEditorProps.editorRef}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const getEditorWindow = async () => {
|
||||
return await getWebViewWindowById('RichTextEditor');
|
||||
};
|
||||
|
||||
type EditorWindow = Window&typeof globalThis;
|
||||
const getEditorControl = (window: EditorWindow) => {
|
||||
if ('joplinRichTextEditor_' in window) {
|
||||
return window.joplinRichTextEditor_ as EditorControl;
|
||||
}
|
||||
throw new Error('No editor control found. Is the editor loaded?');
|
||||
};
|
||||
|
||||
const mockTyping = (window: EditorWindow, text: string) => {
|
||||
const document = window.document;
|
||||
const editor = document.querySelector('div[contenteditable]');
|
||||
|
||||
for (const character of text.split('')) {
|
||||
editor.dispatchEvent(new window.KeyboardEvent('keydown', { key: character }));
|
||||
const paragraphs = editor.querySelectorAll('p');
|
||||
(paragraphs[paragraphs.length - 1] ?? editor).appendChild(document.createTextNode(character));
|
||||
editor.dispatchEvent(new window.KeyboardEvent('keyup', { key: character }));
|
||||
}
|
||||
};
|
||||
|
||||
const mockSelectionMovement = (window: EditorWindow, position: number) => {
|
||||
getEditorControl(window).select(position, position);
|
||||
};
|
||||
|
||||
const findElement = async function<ElementType extends Element = Element>(selector: string) {
|
||||
const window = await getEditorWindow();
|
||||
return await waitFor(() => {
|
||||
const element = window.document.querySelector<ElementType>(selector);
|
||||
expect(element).toBeTruthy();
|
||||
return element;
|
||||
}, {
|
||||
onTimeout: (error) => {
|
||||
return new Error(`Failed to find element from selector ${selector}. DOM: ${window?.document?.body?.innerHTML}. \n\nFull error: ${error}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createRemoteResourceAndNote = async (remoteClientId: number) => {
|
||||
await setupDatabaseAndSynchronizer(remoteClientId);
|
||||
await switchClient(remoteClientId);
|
||||
|
||||
let note = await Note.save({ title: 'Note 1', parent_id: '' });
|
||||
note = await shim.attachFileToNote(note, `${supportDir}/photo.jpg`);
|
||||
|
||||
const allResources = await Resource.all();
|
||||
expect(allResources.length).toBe(1);
|
||||
const resourceId = allResources[0].id;
|
||||
|
||||
await synchronizerStart();
|
||||
await switchClient(0);
|
||||
await synchronizerStart();
|
||||
|
||||
|
||||
return { noteId: note.id, resourceId };
|
||||
};
|
||||
|
||||
describe('RichTextEditor', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
Setting.setValue('editor.codeView', false);
|
||||
});
|
||||
|
||||
it('should render basic markdown', async () => {
|
||||
render(<WrappedEditor
|
||||
noteBody={'### Test\n\nParagraph `test`'}
|
||||
onBodyChange={jest.fn()}
|
||||
/>);
|
||||
|
||||
const dom = (await getEditorWindow()).document;
|
||||
expect((await findElement('h3')).textContent).toBe('Test');
|
||||
expect(dom.querySelector('p').textContent).toBe('Paragraph test');
|
||||
expect(dom.querySelector('p code').textContent).toBe('test');
|
||||
});
|
||||
|
||||
it('should dispatch events when the editor content changes', async () => {
|
||||
let body = '**bold** normal';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' test');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe('**bold** normal test');
|
||||
});
|
||||
});
|
||||
|
||||
it('should save repeated spaces using nonbreaking spaces', async () => {
|
||||
let body = 'Test';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' test');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe('Test \u00A0test');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render clickable checkboxes', async () => {
|
||||
let body = '- [ ] Test\n- [x] Another test';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const firstCheckbox = await findElement<HTMLInputElement>('input[type=checkbox]');
|
||||
const dom = (await getEditorWindow()).document;
|
||||
const getCheckboxLabel = (checkbox: HTMLElement) => {
|
||||
const labelledByAttr = checkbox.getAttribute('aria-labelledby');
|
||||
const label = dom.getElementById(labelledByAttr);
|
||||
return label;
|
||||
};
|
||||
|
||||
// Should have the correct labels
|
||||
expect(firstCheckbox.getAttribute('aria-labelledby')).toBeTruthy();
|
||||
expect(getCheckboxLabel(firstCheckbox).textContent).toBe('Test');
|
||||
|
||||
// Should be correctly checked/unchecked
|
||||
expect(firstCheckbox.checked).toBe(false);
|
||||
|
||||
// Clicking a checkbox should toggle it
|
||||
firstCheckbox.click();
|
||||
|
||||
await waitFor(async () => {
|
||||
// At present, lists are saved as non-tight lists:
|
||||
expect(body.trim()).toBe('- [x] Test\n \n- [x] Another test');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reload resource placeholders when the corresponding item downloads', async () => {
|
||||
Setting.setValue('sync.resourceDownloadMode', 'manual');
|
||||
const { noteId, resourceId } = await createRemoteResourceAndNote(1);
|
||||
|
||||
const note = await Note.load(noteId);
|
||||
const localResource = await Resource.load(resourceId);
|
||||
let localState = await Resource.localState(localResource);
|
||||
expect(localState.fetch_status).toBe(Resource.FETCH_STATUS_IDLE);
|
||||
|
||||
const editorRef = React.createRef<EditorControl>();
|
||||
const component = render(
|
||||
<WrappedEditor
|
||||
noteBody={note.body}
|
||||
noteResources={{ [localResource.id]: { localState, item: localResource } }}
|
||||
onBodyChange={jest.fn()}
|
||||
ref={editorRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The resource placeholder should have rendered
|
||||
const placeholder = await findElement(`span[data-resource-id=${JSON.stringify(localResource.id)}]`);
|
||||
expect([...placeholder.classList]).toContain('not-loaded-resource');
|
||||
|
||||
await resourceFetcher().markForDownload([localResource.id]);
|
||||
|
||||
await waitFor(async () => {
|
||||
localState = await Resource.localState(localResource.id);
|
||||
expect(localState).toMatchObject({ fetch_status: Resource.FETCH_STATUS_DONE });
|
||||
});
|
||||
|
||||
component.rerender(
|
||||
<WrappedEditor
|
||||
noteBody={note.body}
|
||||
noteResources={{ [localResource.id]: { localState, item: localResource } }}
|
||||
onBodyChange={jest.fn()}
|
||||
ref={editorRef}
|
||||
/>,
|
||||
);
|
||||
editorRef.current.onResourceDownloaded(localResource.id);
|
||||
|
||||
expect(
|
||||
await findElement(`img[data-resource-id=${JSON.stringify(localResource.id)}]`),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render clickable internal note links', async () => {
|
||||
const linkTarget = await Note.save({ title: 'test' });
|
||||
const body = `[link](:/${linkTarget.id})`;
|
||||
const onLinkClick = jest.fn();
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={jest.fn()}
|
||||
onLinkClick={onLinkClick}
|
||||
/>);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
|
||||
const link = await findElement<HTMLAnchorElement>('a[href]');
|
||||
expect(link.href).toBe(`:/${linkTarget.id}`);
|
||||
mockSelectionMovement(window, 2);
|
||||
|
||||
const tooltipButton = await findElement<HTMLButtonElement>('.link-tooltip:not(.-hidden) > button');
|
||||
tooltipButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onLinkClick).toHaveBeenCalledWith(`:/${linkTarget.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
MarkupLanguage.Markdown, MarkupLanguage.Html,
|
||||
])('should preserve image attachments on edit (case %#)', async (markupLanguage) => {
|
||||
const { note, resource } = await createNoteAndResource({ markupLanguage });
|
||||
let body = note.body;
|
||||
|
||||
const resources = await attachedResources(body);
|
||||
render(<WrappedEditor
|
||||
noteBody={note.body}
|
||||
note={note}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
noteResources={resources}
|
||||
/>);
|
||||
|
||||
const renderedImage = await findElement<HTMLImageElement>(`img[data-resource-id=${JSON.stringify(resource.id)}]`);
|
||||
expect(renderedImage).toBeTruthy();
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' test');
|
||||
|
||||
// The rendered image should still have the correct ALT and source
|
||||
await waitFor(async () => {
|
||||
const editorContent = body.trim();
|
||||
if (markupLanguage === MarkupLanguage.Html) {
|
||||
expect(editorContent).toMatch(
|
||||
new RegExp(`^<p><img src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
|
||||
);
|
||||
} else {
|
||||
expect(editorContent).toBe(` test`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ useValidSyntax: false },
|
||||
{ useValidSyntax: true },
|
||||
])('should preserve inline math on edit (%j)', async ({ useValidSyntax }) => {
|
||||
const macros = '\\def\\<{\\langle} \\def\\>{\\rangle}';
|
||||
let inlineMath = '| \\< u, v \\> |^2 \\leq \\< u, u \\>\\< v, v \\>';
|
||||
// The \\< escapes are invalid without the above custom macro definitions.
|
||||
// It should be possible for the editor to preserve invalid math syntax.
|
||||
if (useValidSyntax) {
|
||||
inlineMath = macros + inlineMath;
|
||||
}
|
||||
|
||||
let body = `Inline math: $${inlineMath}$...`;
|
||||
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const renderedInlineMath = await findElement<HTMLElement>('span.joplin-editable');
|
||||
expect(renderedInlineMath).toBeTruthy();
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' testing');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe(`Inline math: $${inlineMath}$... testing`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve block math on edit', async () => {
|
||||
let body = 'Test:\n\n$$3^2 + 4^2 = \\sqrt{625}$$\n\nTest.';
|
||||
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const renderedInlineMath = await findElement<HTMLElement>('div.joplin-editable');
|
||||
expect(renderedInlineMath).toBeTruthy();
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' testing');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe('Test:\n\n$$\n3^2 + 4^2 = \\sqrt{625}\n$$\n\nTest. testing');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible show an editor for math blocks', async () => {
|
||||
let body = 'Test:\n\n$$3^2 + 4^2 = 5^2$$';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const editButton = await findElement<HTMLButtonElement>('button.edit');
|
||||
editButton.click();
|
||||
|
||||
const editor = await findElement('dialog .cm-editor');
|
||||
expect(editor).toBeTruthy();
|
||||
expect(editor.textContent).toContain('3^2 + 4^2 = 5^2');
|
||||
});
|
||||
|
||||
it('should preserve table of contents blocks on edit', async () => {
|
||||
let body = '# Heading\n\n# Heading 2\n\n[toc]\n\nTest.';
|
||||
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
// Should render the [toc] as a joplin-editable
|
||||
const renderedTableOfContents = await findElement<HTMLElement>('div.joplin-editable');
|
||||
expect(renderedTableOfContents).toBeTruthy();
|
||||
// Should have a link for each heading
|
||||
expect(renderedTableOfContents.querySelectorAll('a[href]')).toHaveLength(2);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' testing');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe('# Heading\n\n# Heading 2\n\n[toc]\n\nTest. testing');
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
'**bold**',
|
||||
'*italic*',
|
||||
'$\\text{math}$',
|
||||
'<span style="color: red;">test</span>',
|
||||
'`code`',
|
||||
'==highlight==ed',
|
||||
'<sup>Super</sup>script',
|
||||
'<sub>Sub</sub>script',
|
||||
])('should preserve inline markup on edit (case %#)', async (initialBody) => {
|
||||
initialBody += 'test'; // Ensure that typing will add new content outside the formatting
|
||||
let body = initialBody;
|
||||
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
await findElement<HTMLElement>('div.prosemirror-editor');
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' testing');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe(`${initialBody} testing`);
|
||||
});
|
||||
});
|
||||
});
|
156
packages/app-mobile/components/NoteEditor/RichTextEditor.tsx
Normal file
156
packages/app-mobile/components/NoteEditor/RichTextEditor.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useMemo, useCallback, useRef } from 'react';
|
||||
import { NativeSyntheticEvent } from 'react-native';
|
||||
|
||||
import { EditorProps } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
import useWebViewSetup from '../../contentScripts/richTextEditorBundle/useWebViewSetup';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const logger = Logger.create('RichTextEditor');
|
||||
|
||||
function useCss(themeId: number, editorCss: string): string {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const themeVariableCss = themeToCss(theme);
|
||||
return `
|
||||
${themeVariableCss}
|
||||
${editorCss}
|
||||
|
||||
:root {
|
||||
background-color: ${theme.backgroundColor};
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
/* Prefer 100% -- 100vw shows an unnecessary horizontal scrollbar in Google Chrome (desktop). */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 10px;
|
||||
|
||||
font-size: 13pt;
|
||||
font-family: ${JSON.stringify(theme.fontFamily)}, sans-serif;
|
||||
}
|
||||
`;
|
||||
}, [themeId, editorCss]);
|
||||
}
|
||||
|
||||
function useHtml(initialCss: string): string {
|
||||
const cssRef = useRef(initialCss);
|
||||
cssRef.current = initialCss;
|
||||
|
||||
return useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>${_('Note editor')}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="RichTextEditor" style="height:100%;" autocapitalize="on"></div>
|
||||
</body>
|
||||
</html>
|
||||
`, []);
|
||||
}
|
||||
|
||||
const onPostMessage = async (message: string) => {
|
||||
try {
|
||||
await CommandService.instance().execute('openItem', message);
|
||||
} catch (error) {
|
||||
void shim.showErrorDialog(`postMessage failed: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const RichTextEditor: React.FC<EditorProps> = props => {
|
||||
const webviewRef = props.webviewRef;
|
||||
|
||||
const editorWebViewSetup = useWebViewSetup({
|
||||
parentElementClassName: 'RichTextEditor',
|
||||
onEditorEvent: props.onEditorEvent,
|
||||
initialText: props.initialText,
|
||||
noteId: props.noteId,
|
||||
settings: props.editorSettings,
|
||||
globalSearch: props.globalSearch,
|
||||
webviewRef,
|
||||
themeId: props.themeId,
|
||||
pluginStates: props.plugins,
|
||||
noteResources: props.noteResources,
|
||||
onPostMessage: onPostMessage,
|
||||
onAttachFile: props.onAttach,
|
||||
});
|
||||
|
||||
props.editorRef.current = editorWebViewSetup.api;
|
||||
|
||||
const injectedJavaScript = `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
console.error(message);
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
window.onunhandledrejection = (event) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: Unhandled promise rejection: " + event
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
${editorWebViewSetup.pageSetup.js}
|
||||
} catch (e) {
|
||||
console.error('Setup error: ', e);
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
}
|
||||
|
||||
true;
|
||||
`;
|
||||
|
||||
const css = useCss(props.themeId, editorWebViewSetup.pageSetup.css);
|
||||
const html = useHtml(css);
|
||||
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
if (typeof data === 'string' && data.indexOf('error:') === 0) {
|
||||
logger.error('Rich Text Editor error', data);
|
||||
return;
|
||||
}
|
||||
|
||||
editorWebViewSetup.webViewEventHandlers.onMessage(event);
|
||||
}, [editorWebViewSetup]);
|
||||
|
||||
const onError = useCallback((event: NativeSyntheticEvent<WebViewErrorEvent>) => {
|
||||
logger.error(`Load error: Code ${event.nativeEvent.code}: ${event.nativeEvent.description}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
ref={webviewRef}
|
||||
webviewInstanceId='RichTextEditor'
|
||||
testID='RichTextEditor'
|
||||
scrollEnabled={true}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
css={css}
|
||||
hasPluginScripts={false}
|
||||
onMessage={onMessage}
|
||||
onLoadEnd={editorWebViewSetup.webViewEventHandlers.onLoadEnd}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextEditor;
|
@@ -116,6 +116,7 @@ const useStyles = (theme: Theme) => {
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
},
|
||||
input: {
|
||||
flexBasis: 0,
|
||||
flexGrow: 1,
|
||||
height: buttonSize,
|
||||
backgroundColor: theme.backgroundColor4,
|
||||
@@ -136,6 +137,13 @@ const useStyles = (theme: Theme) => {
|
||||
justifyContent: 'center',
|
||||
marginLeft: 10,
|
||||
},
|
||||
panelContainer: {
|
||||
// Workaround for the editor disappearing when dismissing search on Android.
|
||||
// See https://github.com/laurent22/joplin/issues/12781
|
||||
//
|
||||
// It may be possible to remove this line after upgrading to React Native's New Architecture.
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
});
|
||||
}, [theme]);
|
||||
};
|
||||
@@ -171,6 +179,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
returnKeyType='search'
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={control.findNext}
|
||||
selectTextOnFocus={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -192,7 +201,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
}, [state.dialogVisible, control]);
|
||||
|
||||
|
||||
const themeId = props.editorSettings.themeId;
|
||||
const themeId = props.editorSettings.themeData.themeId;
|
||||
const closeButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
@@ -366,7 +375,9 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return showingAdvanced ? advancedLayout : simpleLayout;
|
||||
return <View style={styles.panelContainer}>
|
||||
{showingAdvanced ? advancedLayout : simpleLayout}
|
||||
</View>;
|
||||
};
|
||||
|
||||
export default SearchPanel;
|
||||
|
47
packages/app-mobile/components/NoteEditor/WarningBanner.tsx
Normal file
47
packages/app-mobile/components/NoteEditor/WarningBanner.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
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 { AppState } from '../../utils/types';
|
||||
import { EditorType } from './types';
|
||||
import { Banner } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface Props {
|
||||
editorType: EditorType;
|
||||
richTextBannerDismissed: boolean;
|
||||
}
|
||||
|
||||
const WarningBanner: React.FC<Props> = props => {
|
||||
const actions = useMemo(() => [
|
||||
{
|
||||
label: _('Read more'),
|
||||
onPress: onRichTextReadMoreLinkClick,
|
||||
},
|
||||
{
|
||||
label: _('Dismiss'),
|
||||
accessibilityHint: _('Hides warning'),
|
||||
onPress: onRichTextDismissLinkClick,
|
||||
},
|
||||
], []);
|
||||
|
||||
if (props.editorType !== EditorType.RichText || props.richTextBannerDismissed) return null;
|
||||
return (
|
||||
<Banner
|
||||
icon='alert-outline'
|
||||
actions={actions}
|
||||
// Avoid hiding with react-native-paper's "visible" prop to avoid potential accessibility issues
|
||||
// related to how react-native-paper hides the banner.
|
||||
visible={true}
|
||||
>
|
||||
{_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')}
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
};
|
||||
})(WarningBanner);
|
@@ -2,11 +2,22 @@ import { EditorCommandType } from '@joplin/editor/types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const enabledCondition = (_commandName: string) => {
|
||||
const markdownEditorOnlyCommands = [
|
||||
EditorCommandType.DuplicateLine,
|
||||
EditorCommandType.SortSelectedLines,
|
||||
EditorCommandType.SwapLineUp,
|
||||
EditorCommandType.SwapLineDown,
|
||||
].map(command => `editor.${command}`);
|
||||
|
||||
export const enabledCondition = (commandName: string) => {
|
||||
const output = [
|
||||
'!noteIsReadOnly',
|
||||
];
|
||||
|
||||
if (markdownEditorOnlyCommands.includes(commandName)) {
|
||||
output.push('!richTextEditorVisible');
|
||||
}
|
||||
|
||||
return output.filter(c => !!c).join(' && ');
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import createEditorSettings from '@joplin/editor/testing/createEditorSettings';
|
||||
import { EditorProps } from '../types';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const defaultEditorSettings = { ...createEditorSettings(Setting.THEME_LIGHT), themeId: Setting.THEME_LIGHT };
|
||||
const defaultWrapperProps: EditorProps = {
|
||||
noteResources: {},
|
||||
webviewRef: React.createRef(),
|
||||
editorRef: React.createRef(),
|
||||
themeId: Setting.THEME_LIGHT,
|
||||
noteHash: '',
|
||||
noteId: '',
|
||||
initialText: '',
|
||||
editorSettings: defaultEditorSettings,
|
||||
initialSelection: { start: 0, end: 0 },
|
||||
globalSearch: '',
|
||||
plugins: {},
|
||||
onAttach: () => Promise.resolve(),
|
||||
onEditorEvent: () => {},
|
||||
};
|
||||
|
||||
const createTestEditorProps = () => ({ ...defaultWrapperProps });
|
||||
export default createTestEditorProps;
|
@@ -1,7 +1,12 @@
|
||||
// Types related to the NoteEditor
|
||||
|
||||
import { EditorEvent } from '@joplin/editor/events';
|
||||
import { EditorControl as EditorBodyControl, EditorSettings as EditorBodySettings, SearchState } from '@joplin/editor/types';
|
||||
import { RefObject } from 'react';
|
||||
import { WebViewControl } from '../ExtendedWebView/types';
|
||||
import { SelectionRange } from '../../contentScripts/markdownEditorBundle/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { EditorEvent } from '@joplin/editor/events';
|
||||
import { ResourceInfos } from '@joplin/renderer/types';
|
||||
|
||||
export interface SearchControl {
|
||||
findNext(): void;
|
||||
@@ -45,17 +50,27 @@ export interface EditorControl extends EditorBodyControl {
|
||||
searchControl: SearchControl;
|
||||
}
|
||||
|
||||
export interface EditorSettings extends EditorBodySettings {
|
||||
export type EditorSettings = EditorBodySettings;
|
||||
|
||||
type OnAttachCallback = (mime: string, base64: string)=> Promise<void>;
|
||||
export interface EditorProps {
|
||||
noteResources: ResourceInfos;
|
||||
editorRef: RefObject<EditorBodyControl>;
|
||||
webviewRef: RefObject<WebViewControl>;
|
||||
themeId: number;
|
||||
noteId: string;
|
||||
noteHash: string;
|
||||
initialText: string;
|
||||
initialSelection: SelectionRange;
|
||||
editorSettings: EditorSettings;
|
||||
globalSearch: string;
|
||||
plugins: PluginStates;
|
||||
|
||||
onAttach: OnAttachCallback;
|
||||
onEditorEvent: (event: EditorEvent)=> void;
|
||||
}
|
||||
|
||||
export interface SelectionRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface WebViewToEditorApi {
|
||||
onEditorEvent(event: EditorEvent): Promise<void>;
|
||||
logMessage(message: string): Promise<void>;
|
||||
onPasteFile(type: string, dataBase64: string): Promise<void>;
|
||||
export enum EditorType {
|
||||
Markdown = 'markdown',
|
||||
RichText = 'rich-text',
|
||||
}
|
||||
|
@@ -1,38 +0,0 @@
|
||||
import { View, Platform, SafeAreaView, StyleSheet /* , StatusBar */ } from 'react-native';
|
||||
const React = require('react');
|
||||
// import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
// Untested! This should check if the device has a notch and, if it does, apply
|
||||
// an extra padding on top of the screen.
|
||||
const styles = StyleSheet.create({
|
||||
AndroidSafeArea: {
|
||||
// Disabled for now because it seems that even when there's a notch the system status bar
|
||||
// covers it, and thus we should add our own gap.
|
||||
// Can only test on emulator though
|
||||
// Fixes: https://github.com/laurent22/joplin/issues/2694
|
||||
|
||||
// paddingTop: Platform.OS === 'android' && DeviceInfo.hasNotch() ? StatusBar.currentHeight : 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
});
|
||||
|
||||
function JoplinSafeAreaView(props) {
|
||||
if (Platform.OS === 'ios') {
|
||||
return <SafeAreaView {...props}>{props.children}</SafeAreaView>;
|
||||
} else {
|
||||
const viewProps = { ...props };
|
||||
|
||||
const style = [];
|
||||
|
||||
if (viewProps.style) {
|
||||
style.push(viewProps.style);
|
||||
delete viewProps.style;
|
||||
}
|
||||
|
||||
style.push(styles.AndroidSafeArea);
|
||||
|
||||
return <View style={style} {...viewProps}>{props.children}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JoplinSafeAreaView;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user