Compare commits
309 Commits
cli-v3.3.1
...
sharing_bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53bb2d167 | ||
|
|
4231f8cced | ||
|
|
3f9c60dd10 | ||
|
|
35e189ef6e | ||
|
|
a15bad37b1 | ||
|
|
8b4ad0aaf7 | ||
|
|
c3575672b2 | ||
|
|
e840d0c3fd | ||
|
|
5227ba1adb | ||
|
|
ea49907327 | ||
|
|
0dd90a7542 | ||
|
|
a962f48b38 | ||
|
|
f68d2bbc7c | ||
|
|
65c9665a2a | ||
|
|
2c50ad36c5 | ||
|
|
7212269107 | ||
|
|
1387470f2a | ||
|
|
a6d5eb9b8e | ||
|
|
5d1a055d2a | ||
|
|
36910a2a9b | ||
|
|
b4a57a10aa | ||
|
|
bca8cb1c2d | ||
|
|
0b489a9c98 | ||
|
|
ce32651794 | ||
|
|
f0159cdd89 | ||
|
|
97652fa362 | ||
|
|
113b259b81 | ||
|
|
2af895477f | ||
|
|
c7e31d1ac9 | ||
|
|
c51b13ca73 | ||
|
|
f5febb18b4 | ||
|
|
9d6aa1c739 | ||
|
|
3b27f84996 | ||
|
|
fc38691f3a | ||
|
|
d2274319f9 | ||
|
|
a40448fed9 | ||
|
|
5ec79c74e2 | ||
|
|
bbba19eb40 | ||
|
|
75b89c7e09 | ||
|
|
f9af9a724c | ||
|
|
6e7c9c059d | ||
|
|
69ee435b0b | ||
|
|
204f1bf509 | ||
|
|
7a7a2c4cec | ||
|
|
441486acaa | ||
|
|
4684142df7 | ||
|
|
0a871ea44b | ||
|
|
901fe73c08 | ||
|
|
41553eb963 | ||
|
|
cada200575 | ||
|
|
13711c6a9c | ||
|
|
1a6acee5c8 | ||
|
|
0c2547a780 | ||
|
|
e0204d672b | ||
|
|
9c9b06de2d | ||
|
|
58f3344564 | ||
|
|
f6fef5a8ec | ||
|
|
e0211045db | ||
|
|
f757221d44 | ||
|
|
552ecc9064 | ||
|
|
7d4864193f | ||
|
|
81e2205a53 | ||
|
|
4e89890a23 | ||
|
|
60de33b8be | ||
|
|
84d6f5dfcb | ||
|
|
d0d80c0e4a | ||
|
|
798c1e1c2b | ||
|
|
1eef44d243 | ||
|
|
e5adaa7f74 | ||
|
|
671997af96 | ||
|
|
2bf968f9ad | ||
|
|
3e06dd989f | ||
|
|
3459355285 | ||
|
|
7406a89dc0 | ||
|
|
ace662cc79 | ||
|
|
0c5d5e59f3 | ||
|
|
b00aadb542 | ||
|
|
d6883e6ec1 | ||
|
|
6ac64ca0d9 | ||
|
|
9890d267a1 | ||
|
|
1a1335a7d5 | ||
|
|
67288f0b44 | ||
|
|
a0cd09cd5b | ||
|
|
6e5623ce6a | ||
|
|
032f26b1c5 | ||
|
|
d0030a904c | ||
|
|
a23d5d10b6 | ||
|
|
f9ccd15615 | ||
|
|
1f9f63d176 | ||
|
|
813f077312 | ||
|
|
6a5c85d3d7 | ||
|
|
1644f56447 | ||
|
|
85518edca1 | ||
|
|
ebc070b3c7 | ||
|
|
a33fb575fd | ||
|
|
ecc781ee39 | ||
|
|
098cabad40 | ||
|
|
4d01738029 | ||
|
|
3433293a0e | ||
|
|
02fd244096 | ||
|
|
00cd26fd82 | ||
|
|
38ca224a16 | ||
|
|
0fec577932 | ||
|
|
780d049502 | ||
|
|
a5d74e1ee7 | ||
|
|
d6b369b4f4 | ||
|
|
572e40c635 | ||
|
|
4af5c609fd | ||
|
|
8487fc1a34 | ||
|
|
a76fad3ddf | ||
|
|
a08af91153 | ||
|
|
3bcf221e52 | ||
|
|
0dd211c2fd | ||
|
|
b6fea2a4e2 | ||
|
|
73eb6cca38 | ||
|
|
449f49379d | ||
|
|
c4b951544b | ||
|
|
5746d4cdf6 | ||
|
|
71e4f35e79 | ||
|
|
5169371b68 | ||
|
|
24845bd7d8 | ||
|
|
00b7726cda | ||
|
|
ce9008998c | ||
|
|
776813acfe | ||
|
|
d13c213657 | ||
|
|
1895b3d067 | ||
|
|
863d00c595 | ||
|
|
1b8b1f7b2a | ||
|
|
ca8b68bd95 | ||
|
|
3c3e7c4854 | ||
|
|
aeab8e03ab | ||
|
|
a3cc34938b | ||
|
|
ec057e4e2e | ||
|
|
78eca9dfa9 | ||
|
|
174cc76ef4 | ||
|
|
ba5bb6e8ea | ||
|
|
0a8a19748e | ||
|
|
7884ce61a0 | ||
|
|
d7f2ffaa1e | ||
|
|
a79409dfa8 | ||
|
|
4777bd393c | ||
|
|
c7575e4726 | ||
|
|
efc5059d65 | ||
|
|
bd203ffe88 | ||
|
|
27717b46ac | ||
|
|
364b2496d6 | ||
|
|
b701992524 | ||
|
|
d00bd3b89d | ||
|
|
c80e789a8d | ||
|
|
503b31e67a | ||
|
|
1b5e538d6a | ||
|
|
c8cbe7271e | ||
|
|
7330efceaf | ||
|
|
da2229706c | ||
|
|
b81f5cb91e | ||
|
|
3b7a677302 | ||
|
|
0d176e434a | ||
|
|
ca46df5627 | ||
|
|
7389712093 | ||
|
|
3f3f7328f9 | ||
|
|
fda69c7a1e | ||
|
|
66ec4f8c51 | ||
|
|
d62ac838b8 | ||
|
|
487cb4f743 | ||
|
|
8eef48ac4b | ||
|
|
42a156c2bb | ||
|
|
caba91fdf6 | ||
|
|
360446cc79 | ||
|
|
8c0d5f4ac5 | ||
|
|
696fe4d5a6 | ||
|
|
c9027719dd | ||
|
|
c9936723c8 | ||
|
|
f6056b2d75 | ||
|
|
303ccce7d2 | ||
|
|
7a611ac5c5 | ||
|
|
45d1d862a1 | ||
|
|
99178fb1c6 | ||
|
|
1462284f2f | ||
|
|
c392854bdf | ||
|
|
2ebb3f039d | ||
|
|
c8fd9a2b39 | ||
|
|
869b1e6f98 | ||
|
|
ee04f28356 | ||
|
|
d3ac7ad1c3 | ||
|
|
da4e3fc5bb | ||
|
|
73808f5a25 | ||
|
|
c887a86fd8 | ||
|
|
b0d888ee73 | ||
|
|
d9c9bed393 | ||
|
|
f16b6e8887 | ||
|
|
9c8fcbe0c2 | ||
|
|
6760468da3 | ||
|
|
3d3e8a70fa | ||
|
|
9b65123335 | ||
|
|
8493decc03 | ||
|
|
c28e838f17 | ||
|
|
22779a7f15 | ||
|
|
f2aac66e56 | ||
|
|
b956da47fa | ||
|
|
8134390bf4 | ||
|
|
631211b40c | ||
|
|
144ed593cc | ||
|
|
47d0d3eb9e | ||
|
|
4498c5bc0f | ||
|
|
1716562292 | ||
|
|
efcfd12489 | ||
|
|
42f6a9d03d | ||
|
|
73a2075a69 | ||
|
|
c5ca0151a1 | ||
|
|
9ffeb8c725 | ||
|
|
884141c3e2 | ||
|
|
f0d1dd1dd0 | ||
|
|
70e7d8f820 | ||
|
|
100c35cf7f | ||
|
|
5b42f4f2a2 | ||
|
|
a47d7906af | ||
|
|
73ed17e851 | ||
|
|
484deb450b | ||
|
|
3d2ac91b8a | ||
|
|
0fc665d6d8 | ||
|
|
961349c1f3 | ||
|
|
ab95d728d9 | ||
|
|
ca653d3e88 | ||
|
|
945b309a4d | ||
|
|
ab17625ed8 | ||
|
|
8e8ab3bd80 | ||
|
|
591a56e510 | ||
|
|
86ee95a8d0 | ||
|
|
a527a278a9 | ||
|
|
eb1970fd1a | ||
|
|
1ecaaa1910 | ||
|
|
c75b48fbb1 | ||
|
|
608dbab453 | ||
|
|
291ba88224 | ||
|
|
bdbd16240b | ||
|
|
4f826c045a | ||
|
|
885fde4119 | ||
|
|
f8200efffc | ||
|
|
700ddf269a | ||
|
|
a755f09033 | ||
|
|
293eac9c04 | ||
|
|
f5e751c27a | ||
|
|
47e4f36f97 | ||
|
|
fa0dbddb9b | ||
|
|
c9eb9af741 | ||
|
|
d1cd8e9db4 | ||
|
|
16ebff78b4 | ||
|
|
423ae0d633 | ||
|
|
88c95cc91c | ||
|
|
24df674726 | ||
|
|
3faa95a066 | ||
|
|
60e27924ae | ||
|
|
d5830dd3a1 | ||
|
|
a4dacd65e6 | ||
|
|
cbf6d5506f | ||
|
|
80696fe324 | ||
|
|
1780a530c9 | ||
|
|
dc786e8178 | ||
|
|
e65fbecef0 | ||
|
|
5b2f409254 | ||
|
|
77a07c937e | ||
|
|
50c5139fa6 | ||
|
|
465defb194 | ||
|
|
12c688eb83 | ||
|
|
1a6059072a | ||
|
|
e2ef406aa2 | ||
|
|
dca28f8c2a | ||
|
|
a8ea71c349 | ||
|
|
b13e7c1a3f | ||
|
|
ffd9c8e0a0 | ||
|
|
a4b0d149bc | ||
|
|
c2b7228170 | ||
|
|
11c8f1e111 | ||
|
|
a1f37dc414 | ||
|
|
e4a20f505d | ||
|
|
dfa7ed0a5c | ||
|
|
f8bf799f00 | ||
|
|
c70faf50f7 | ||
|
|
b6043489a0 | ||
|
|
37270479e2 | ||
|
|
318ca3de5f | ||
|
|
1f57a94225 | ||
|
|
cce2f66f01 | ||
|
|
277935b8b1 | ||
|
|
981759691d | ||
|
|
370f6bd70e | ||
|
|
067ce65532 | ||
|
|
302577ed43 | ||
|
|
5356a8ae36 | ||
|
|
4d790b6ffe | ||
|
|
b6d8dcee8d | ||
|
|
e5b2e22479 | ||
|
|
6e98a8ac2d | ||
|
|
6b60a88dcb | ||
|
|
765cde10fb | ||
|
|
8f97bb6ddf | ||
|
|
073984b9ef | ||
|
|
5e5a77786c | ||
|
|
7fa3a223b2 | ||
|
|
5c965d4b95 | ||
|
|
cc3da4fd09 | ||
|
|
34528d146f | ||
|
|
9c76560642 | ||
|
|
3cb621eb60 | ||
|
|
d9ba532889 | ||
|
|
5aca4bc72e | ||
|
|
54e7499aa2 | ||
|
|
941b70471f | ||
|
|
3f364a4a9b |
30
.env-transcribe-sample
Normal file
@@ -0,0 +1,30 @@
|
||||
# =============================================================================
|
||||
# Required
|
||||
# -----------------------------------------------------------------------------
|
||||
# =============================================================================
|
||||
|
||||
SERVER_PORT=4567
|
||||
|
||||
API_KEY=random-string
|
||||
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
|
||||
|
||||
QUEUE_DRIVER=pg
|
||||
# QUEUE_DRIVER=sqlite
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Queue driver
|
||||
# -----------------------------------------------------------------------------
|
||||
# =============================================================================
|
||||
#
|
||||
# QUEUE_DATABASE_NAME=./queue.sqlite3
|
||||
QUEUE_DATABASE_NAME=transcribe
|
||||
QUEUE_DATABASE_USER=transcribe
|
||||
QUEUE_DATABASE_PASSWORD=transcribe
|
||||
QUEUE_DATABASE_PORT=5432
|
||||
@@ -78,6 +78,7 @@ packages/plugins/**/api
|
||||
packages/plugins/**/dist
|
||||
packages/server/dist/
|
||||
packages/utils/dist/
|
||||
packages/transcribe/dist/
|
||||
packages/tools/node_modules
|
||||
packages/tools/PortableAppsLauncher
|
||||
packages/turndown-plugin-gfm/
|
||||
@@ -121,6 +122,8 @@ packages/app-cli/app/command-rmnote.test.js
|
||||
packages/app-cli/app/command-rmnote.js
|
||||
packages/app-cli/app/command-set.js
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-share.test.js
|
||||
packages/app-cli/app/command-share.js
|
||||
packages/app-cli/app/command-sync.js
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-use.js
|
||||
@@ -129,6 +132,8 @@ packages/app-cli/app/gui/FolderListWidget.js
|
||||
packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
packages/app-cli/app/utils/initializeCommandService.js
|
||||
packages/app-cli/app/utils/shimInitCli.js
|
||||
packages/app-cli/app/utils/testUtils.js
|
||||
packages/app-cli/tests/HtmlToMd.js
|
||||
packages/app-cli/tests/MarkupToHtml.js
|
||||
@@ -151,6 +156,7 @@ packages/app-desktop/app.js
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyToClipboard.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/emptyTrash.js
|
||||
packages/app-desktop/commands/exportDeletionLog.test.js
|
||||
@@ -296,6 +302,7 @@ packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useConnectToEditorPlugin.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
@@ -419,6 +426,7 @@ packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js
|
||||
packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js
|
||||
packages/app-desktop/gui/Sidebar/types.js
|
||||
packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
@@ -445,7 +453,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
@@ -534,12 +541,15 @@ packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/evaluateWithRetry.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/setSettingValue.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
@@ -573,6 +583,7 @@ packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/bundleJs.js
|
||||
packages/app-desktop/tools/copy7Zip.js
|
||||
packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
@@ -587,11 +598,13 @@ packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/getAssetPath.js
|
||||
packages/app-desktop/utils/initializeCommandService.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/sourceMapSetup.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
@@ -608,12 +621,16 @@ packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.web.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
@@ -803,6 +820,8 @@ packages/app-mobile/components/screens/Note/commands/insertDateTime.js
|
||||
packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteRevisionViewer.test.js
|
||||
packages/app-mobile/components/screens/NoteRevisionViewer.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
@@ -813,6 +832,9 @@ packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/ShareNoteDialog.test.js
|
||||
packages/app-mobile/components/screens/ShareNoteDialog.js
|
||||
packages/app-mobile/components/screens/SsoLoginScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
@@ -874,6 +896,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
@@ -890,6 +913,7 @@ packages/app-mobile/utils/makeShowMessageBox.test.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
@@ -902,6 +926,7 @@ packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/testing/testingLibrary.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/app-mobile/web/webpack.config.js
|
||||
@@ -912,7 +937,6 @@ packages/default-plugins/commands/editPatch.js
|
||||
packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
@@ -1017,7 +1041,10 @@ packages/generator-joplin/generators/app/templates/api/types.js
|
||||
packages/generator-joplin/generators/app/templates/api_index.js
|
||||
packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/htmlpack/index.test.js
|
||||
packages/htmlpack/index.js
|
||||
packages/htmlpack/packToString.js
|
||||
packages/htmlpack/utils/parseHtmlAsync.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
@@ -1042,6 +1069,7 @@ packages/lib/RotatingLogs.js
|
||||
packages/lib/SyncTargetFilesystem.js
|
||||
packages/lib/SyncTargetJoplinCloud.js
|
||||
packages/lib/SyncTargetJoplinServer.js
|
||||
packages/lib/SyncTargetJoplinServerSAML.js
|
||||
packages/lib/SyncTargetNone.js
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
@@ -1056,6 +1084,7 @@ packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/leaveSharedFolder.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
@@ -1068,6 +1097,15 @@ packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
|
||||
packages/lib/components/shared/SamlShared.js
|
||||
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
|
||||
packages/lib/components/shared/ShareNoteDialog/types.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
|
||||
packages/lib/components/shared/SsoScreenShared.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
@@ -1102,12 +1140,13 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useNowEffect.test.js
|
||||
packages/lib/hooks/useNowEffect.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
@@ -1348,12 +1387,14 @@ packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorViewIds.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
@@ -1509,11 +1550,13 @@ packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud/index.js
|
||||
packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/markupLanguageUtils.js
|
||||
packages/lib/utils/prefixWithHttps.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
packages/lib/utils/resolvePathWithinDir.test.js
|
||||
packages/lib/utils/resolvePathWithinDir.js
|
||||
packages/lib/utils/types/pdfJs.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
@@ -1605,6 +1648,18 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.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/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
|
||||
@@ -23,6 +23,8 @@ module.exports = {
|
||||
'FileSystemCreateWritableOptions': 'readonly',
|
||||
'FileSystemHandle': 'readonly',
|
||||
'IDBTransactionMode': 'readonly',
|
||||
'BigInt': 'readonly',
|
||||
'globalThis': 'readonly',
|
||||
|
||||
// ServiceWorker
|
||||
'ExtendableEvent': 'readonly',
|
||||
|
||||
2
.github/scripts/run_ci.sh
vendored
@@ -117,7 +117,7 @@ if [ "$RUN_TESTS" == "1" ]; then
|
||||
# On Linux, we run the Joplin Server tests using PostgreSQL
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Running Joplin Server tests using PostgreSQL..."
|
||||
sudo docker compose --file docker-compose.db-dev.yml up -d
|
||||
sudo docker compose --parallel 1 --file docker-compose.db-dev.yml up -d
|
||||
cmdResult=$?
|
||||
if [ $cmdResult -ne 0 ]; then
|
||||
exit $cmdResult
|
||||
|
||||
2
.github/workflows/automerge.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- id: automerge
|
||||
name: automerge
|
||||
uses: "pascalgn/automerge-action@v0.16.3"
|
||||
uses: "pascalgn/automerge-action@v0.16.4"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: "squash"
|
||||
|
||||
6
.github/workflows/build-macos-m1.yml
vendored
@@ -8,12 +8,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: olegtarasov/get-tag@v2.1.4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
node-version: '18.20.8'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
# See github-action-main.yml for explanation
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Set Publish Flag
|
||||
run: |
|
||||
|
||||
10
.github/workflows/github-actions-main.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-13, ubuntu-22.04, windows-2019, ubuntu-22.04-arm]
|
||||
os: [macos-13, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -122,7 +122,6 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
@@ -141,11 +140,6 @@ jobs:
|
||||
echo "RUNNER_ARCH=$RUNNER_ARCH"
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
|
||||
cd packages/lib
|
||||
yarn remove canvas
|
||||
cd ../..
|
||||
|
||||
yarn install
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
|
||||
|
||||
@@ -159,7 +153,7 @@ jobs:
|
||||
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
|
||||
# Wait for server to start
|
||||
sleep 30
|
||||
sleep 120
|
||||
|
||||
# Check if status code is correct
|
||||
# if the actual_status DOES NOT include the expected_status
|
||||
|
||||
@@ -5,10 +5,10 @@ runs:
|
||||
steps:
|
||||
# Trying to fix random networking issues on Windows
|
||||
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
|
||||
- name: Disable TCP/UDP offload on Windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
|
||||
# - name: Disable TCP/UDP offload on Windows
|
||||
# if: runner.os == 'Windows'
|
||||
# shell: pwsh
|
||||
# run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
|
||||
|
||||
- name: Disable TCP/UDP offload on Linux
|
||||
if: runner.os == 'Linux'
|
||||
@@ -47,14 +47,17 @@ runs:
|
||||
# Required for building the canvas package
|
||||
brew install pango
|
||||
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: olegtarasov/get-tag@v2.1.4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
node-version: '18.20.8'
|
||||
# Disable the cache on ARM runners. For now, we don't run "yarn install" on these
|
||||
# environments and this breaks actions/setup-node.
|
||||
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
|
||||
# cache: ${{ (!contains(runner.os, 'arm') && 'yarn') || '' }}
|
||||
|
||||
- name: Install Yarn
|
||||
shell: bash
|
||||
@@ -69,4 +72,4 @@ runs:
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '3.13'
|
||||
|
||||
7
.github/workflows/ui-tests.yml
vendored
@@ -9,13 +9,18 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-13, ubuntu-22.04, windows-2025]
|
||||
os: [ubuntu-22.04, windows-2025]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup build environment
|
||||
uses: ./.github/workflows/shared/setup-build-environment
|
||||
- name: Build
|
||||
run: yarn install
|
||||
env:
|
||||
# The onenote-converter package uses Rust, which isn't installed on all CI
|
||||
# runners. Since the onenote-converter doesn't have UI tests, it can be excluded
|
||||
# from build:
|
||||
SKIP_ONENOTE_CONVERTER_BUILD: 1
|
||||
- name: Run UI tests
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
64
.gitignore
vendored
@@ -46,6 +46,7 @@ sync_staging.sh
|
||||
TODO.md
|
||||
packages/tools/commit_hook.txt
|
||||
packages/tools/github_oauth_token.txt
|
||||
packages/app-desktop/main-html-out.js
|
||||
lerna-debug.log
|
||||
.env
|
||||
docs/**/*.mustache
|
||||
@@ -58,6 +59,7 @@ docs/**/*.mustache
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/joplin-empty-package
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
@@ -95,6 +97,8 @@ packages/app-cli/app/command-rmnote.test.js
|
||||
packages/app-cli/app/command-rmnote.js
|
||||
packages/app-cli/app/command-set.js
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-share.test.js
|
||||
packages/app-cli/app/command-share.js
|
||||
packages/app-cli/app/command-sync.js
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-use.js
|
||||
@@ -103,6 +107,8 @@ packages/app-cli/app/gui/FolderListWidget.js
|
||||
packages/app-cli/app/gui/StatusBarWidget.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/setupCommand.js
|
||||
packages/app-cli/app/utils/initializeCommandService.js
|
||||
packages/app-cli/app/utils/shimInitCli.js
|
||||
packages/app-cli/app/utils/testUtils.js
|
||||
packages/app-cli/tests/HtmlToMd.js
|
||||
packages/app-cli/tests/MarkupToHtml.js
|
||||
@@ -125,6 +131,7 @@ packages/app-desktop/app.js
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/checkForUpdates.js
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyToClipboard.js
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/emptyTrash.js
|
||||
packages/app-desktop/commands/exportDeletionLog.test.js
|
||||
@@ -270,6 +277,7 @@ packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useConnectToEditorPlugin.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
@@ -393,6 +401,7 @@ packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js
|
||||
packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js
|
||||
packages/app-desktop/gui/Sidebar/types.js
|
||||
packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
@@ -419,7 +428,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
@@ -508,12 +516,15 @@ packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/evaluateWithRetry.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/setSettingValue.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
@@ -547,6 +558,7 @@ packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/bundleJs.js
|
||||
packages/app-desktop/tools/copy7Zip.js
|
||||
packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
@@ -561,11 +573,13 @@ packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/getAssetPath.js
|
||||
packages/app-desktop/utils/initializeCommandService.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/sourceMapSetup.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
@@ -582,12 +596,16 @@ packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.web.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
|
||||
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/testing.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager/PromptButton.js
|
||||
@@ -777,6 +795,8 @@ packages/app-mobile/components/screens/Note/commands/insertDateTime.js
|
||||
packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteRevisionViewer.test.js
|
||||
packages/app-mobile/components/screens/NoteRevisionViewer.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
@@ -787,6 +807,9 @@ packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/ShareNoteDialog.test.js
|
||||
packages/app-mobile/components/screens/ShareNoteDialog.js
|
||||
packages/app-mobile/components/screens/SsoLoginScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
@@ -848,6 +871,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useBackHandler.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
@@ -864,6 +888,7 @@ packages/app-mobile/utils/makeShowMessageBox.test.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
@@ -876,6 +901,7 @@ packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/testing/testingLibrary.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/app-mobile/web/webpack.config.js
|
||||
@@ -886,7 +912,6 @@ packages/default-plugins/commands/editPatch.js
|
||||
packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
@@ -991,7 +1016,10 @@ packages/generator-joplin/generators/app/templates/api/types.js
|
||||
packages/generator-joplin/generators/app/templates/api_index.js
|
||||
packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/htmlpack/index.test.js
|
||||
packages/htmlpack/index.js
|
||||
packages/htmlpack/packToString.js
|
||||
packages/htmlpack/utils/parseHtmlAsync.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
@@ -1016,6 +1044,7 @@ packages/lib/RotatingLogs.js
|
||||
packages/lib/SyncTargetFilesystem.js
|
||||
packages/lib/SyncTargetJoplinCloud.js
|
||||
packages/lib/SyncTargetJoplinServer.js
|
||||
packages/lib/SyncTargetJoplinServerSAML.js
|
||||
packages/lib/SyncTargetNone.js
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
@@ -1030,6 +1059,7 @@ packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/leaveSharedFolder.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
@@ -1042,6 +1072,15 @@ packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
|
||||
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
|
||||
packages/lib/components/shared/SamlShared.js
|
||||
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
|
||||
packages/lib/components/shared/ShareNoteDialog/types.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
|
||||
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
|
||||
packages/lib/components/shared/SsoScreenShared.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/plugins/types.js
|
||||
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
|
||||
@@ -1076,12 +1115,13 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useNowEffect.test.js
|
||||
packages/lib/hooks/useNowEffect.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
@@ -1322,12 +1362,14 @@ packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorViewIds.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
@@ -1483,11 +1525,13 @@ packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud/index.js
|
||||
packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/markupLanguageUtils.js
|
||||
packages/lib/utils/prefixWithHttps.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
packages/lib/utils/resolvePathWithinDir.test.js
|
||||
packages/lib/utils/resolvePathWithinDir.js
|
||||
packages/lib/utils/types/pdfJs.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
@@ -1579,6 +1623,18 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.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/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@joplin/fork-sax",
|
||||
"@joplin/fork-uslug",
|
||||
"@joplin/htmlpack",
|
||||
"@joplin/transcribe",
|
||||
"@joplin/lib",
|
||||
"@joplin/onenote-converter",
|
||||
"@joplin/pdf-viewer",
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"cSpell.enabled": true
|
||||
"cSpell.enabled": true,
|
||||
"editor.insertSpaces": false
|
||||
}
|
||||
10
.yarn/joplin-empty-package/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# @joplin/empty
|
||||
|
||||
An empty package. This package can be used to exclude certain dependencies from build.
|
||||
|
||||
For example, the `canvas` dependency is an optional dependency of `pdfjs-dist`. However, it isn't used by Joplin and can cause build to fail in certain environments. The `@joplin/empty` package can exclude `canvas` from the build by adding a resolution to `resolutions` in the top-level `package.json`. For example, resolving `canvas@npm:^2.11` to `file:./packages/empty/`.
|
||||
|
||||
See also:
|
||||
- [Yarn docs: Manifest resolutions](https://yarnpkg.com/configuration/manifest#resolutions)
|
||||
- [GitHub comment: Yarn: Ignoring packages](https://github.com/yarnpkg/yarn/issues/4611#issuecomment-1370284462)
|
||||
|
||||
10
.yarn/joplin-empty-package/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@joplin/empty",
|
||||
"version": "0.0.0",
|
||||
"description": "An empty package, used as a way to exclude certain packages from build",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/laurent22/joplin.git"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
|
||||
# We remove the `canvas` optional dependency because electron-rebuild fails to build it, and
|
||||
# the `canvas` API is already part of Electron
|
||||
diff --git a/build/pdf.js b/build/pdf.js
|
||||
index 4acf16b1d6f9351bda1a98649ea4f926618fe617..f63dbc6050ca63ca8e8ed982edea134103fa15dd 100644
|
||||
--- a/build/pdf.js
|
||||
+++ b/build/pdf.js
|
||||
@@ -6244,8 +6244,9 @@ class NodeFilterFactory extends _base_factory.BaseFilterFactory {}
|
||||
exports.NodeFilterFactory = NodeFilterFactory;
|
||||
class NodeCanvasFactory extends _base_factory.BaseCanvasFactory {
|
||||
_createCanvas(width, height) {
|
||||
- const Canvas = require("canvas");
|
||||
- return Canvas.createCanvas(width, height);
|
||||
+ throw new Error('Node canvas disabled');
|
||||
+ // const Canvas = require("canvas");
|
||||
+ // return Canvas.createCanvas(width, height);
|
||||
}
|
||||
}
|
||||
exports.NodeCanvasFactory = NodeCanvasFactory;
|
||||
diff --git a/package.json b/package.json
|
||||
index 105811f53d508486e08a60dc1b6e437cd24d7427..dea6a4e6612c4a4006cc482e46ff5270dcfda1e5 100644
|
||||
--- a/package.json
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
index 8a719ca35af1cc3a4192c5c5f8258fd4f7fea990..5f8831f81cd164a4f627423427ead92fa286b115 100644
|
||||
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
@@ -37,7 +37,7 @@ import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
-import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
+import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -149,7 +149,10 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
}
|
||||
|
||||
private class ConcurrentOperationQueue {
|
||||
- private final Queue<UIThreadOperation> mQueue = new ConcurrentLinkedQueue<>();
|
||||
+ // Patch: Use LinkedBlockingQueue instead of ConcurrentLinkedQueue.
|
||||
+ // In some versions of Android, ConcurrentLinkedQueue is known to drop
|
||||
+ // items, causing crashing. See https://github.com/laurent22/joplin/issues/8425
|
||||
+ private final Queue<UIThreadOperation> mQueue = new LinkedBlockingQueue<>();
|
||||
@Nullable private UIThreadOperation mPeekedOperation = null;
|
||||
|
||||
@AnyThread
|
||||
205
.yarn/patches/react-native-npm-0.79.2-9db13eddfe.patch
Normal file
@@ -0,0 +1,205 @@
|
||||
# This patch fixes two issues:
|
||||
# - Updates RCTDeviceInfo.m to match https://github.com/facebook/react-native/commit/0b8db7e5e814cfbf9974cc5b6ceb64e8006d8a3c.
|
||||
# This fixes an issue in which useWindowDimensions returns incorrect
|
||||
# values in landscape mode in iOS.
|
||||
# This should be fixed in React Native 0.80. See https://github.com/facebook/react-native/issues/51086.
|
||||
# - Updates NativeAnimatedModule.java to work around an Android 12-specific crash.
|
||||
diff --git a/React/CoreModules/RCTDeviceInfo.mm b/React/CoreModules/RCTDeviceInfo.mm
|
||||
index 6b4fcef852252e8d4ac2aceb12175fdfafb4def7..8ceab21e8653d429876d10e2d12ed1342780ad7d 100644
|
||||
--- a/React/CoreModules/RCTDeviceInfo.mm
|
||||
+++ b/React/CoreModules/RCTDeviceInfo.mm
|
||||
@@ -14,9 +14,7 @@
|
||||
#import <React/RCTEventDispatcherProtocol.h>
|
||||
#import <React/RCTInitializing.h>
|
||||
#import <React/RCTInvalidating.h>
|
||||
-#import <React/RCTKeyWindowValuesProxy.h>
|
||||
#import <React/RCTUtils.h>
|
||||
-#import <React/RCTWindowSafeAreaProxy.h>
|
||||
#import <atomic>
|
||||
|
||||
#import "CoreModulesPlugins.h"
|
||||
@@ -31,8 +29,13 @@ using namespace facebook::react;
|
||||
NSDictionary *_currentInterfaceDimensions;
|
||||
BOOL _isFullscreen;
|
||||
std::atomic<BOOL> _invalidated;
|
||||
+ NSDictionary *_constants;
|
||||
+
|
||||
+ __weak UIWindow *_applicationWindow;
|
||||
}
|
||||
|
||||
+static NSString *const kFrameKeyPath = @"frame";
|
||||
+
|
||||
@synthesize moduleRegistry = _moduleRegistry;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
@@ -40,14 +43,26 @@ RCT_EXPORT_MODULE()
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
- [[RCTKeyWindowValuesProxy sharedInstance] startObservingWindowSizeIfNecessary];
|
||||
+ _applicationWindow = RCTKeyWindow();
|
||||
+ [_applicationWindow addObserver:self forKeyPath:kFrameKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
+ ofObject:(id)object
|
||||
+ change:(NSDictionary *)change
|
||||
+ context:(void *)context
|
||||
+{
|
||||
+ if ([keyPath isEqualToString:kFrameKeyPath]) {
|
||||
+ [self interfaceFrameDidChange];
|
||||
+ [[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+ (BOOL)requiresMainQueueSetup
|
||||
{
|
||||
- return NO;
|
||||
+ return YES;
|
||||
}
|
||||
|
||||
- (dispatch_queue_t)methodQueue
|
||||
@@ -81,7 +96,7 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
- _currentInterfaceOrientation = [RCTKeyWindowValuesProxy sharedInstance].currentInterfaceOrientation;
|
||||
+ _currentInterfaceOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(interfaceFrameDidChange)
|
||||
@@ -98,6 +113,15 @@ RCT_EXPORT_MODULE()
|
||||
selector:@selector(invalidate)
|
||||
name:RCTBridgeWillInvalidateModulesNotification
|
||||
object:nil];
|
||||
+
|
||||
+ _constants = @{
|
||||
+ @"Dimensions" : [self _exportedDimensions],
|
||||
+ // Note:
|
||||
+ // This prop is deprecated and will be removed in a future release.
|
||||
+ // Please use this only for a quick and temporary solution.
|
||||
+ // Use <SafeAreaView> instead.
|
||||
+ @"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
|
||||
+ };
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
@@ -120,6 +144,8 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTBridgeWillInvalidateModulesNotification object:nil];
|
||||
|
||||
+ [_applicationWindow removeObserver:self forKeyPath:kFrameKeyPath];
|
||||
+
|
||||
#if TARGET_OS_IOS
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
|
||||
#endif
|
||||
@@ -132,8 +158,13 @@ static BOOL RCTIsIPhoneNotched()
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
dispatch_once(&onceToken, ^{
|
||||
+ RCTAssertMainQueue();
|
||||
+
|
||||
// 20pt is the top safeArea value in non-notched devices
|
||||
- isIPhoneNotched = [RCTWindowSafeAreaProxy sharedInstance].currentSafeAreaInsets.top > 20;
|
||||
+ UIWindow *keyWindow = RCTKeyWindow();
|
||||
+ if (keyWindow) {
|
||||
+ isIPhoneNotched = keyWindow.safeAreaInsets.top > 20;
|
||||
+ }
|
||||
});
|
||||
#endif
|
||||
|
||||
@@ -142,11 +173,13 @@ static BOOL RCTIsIPhoneNotched()
|
||||
|
||||
static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
|
||||
{
|
||||
+ RCTAssertMainQueue();
|
||||
UIScreen *mainScreen = UIScreen.mainScreen;
|
||||
CGSize screenSize = mainScreen.bounds.size;
|
||||
+ UIView *mainWindow = RCTKeyWindow();
|
||||
|
||||
// We fallback to screen size if a key window is not found.
|
||||
- CGSize windowSize = [RCTKeyWindowValuesProxy sharedInstance].windowSize;
|
||||
+ CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize;
|
||||
|
||||
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
|
||||
@"width" : @(windowSize.width),
|
||||
@@ -170,7 +203,10 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
|
||||
RCTAssert(_moduleRegistry, @"Failed to get exported dimensions: RCTModuleRegistry is nil");
|
||||
RCTAccessibilityManager *accessibilityManager =
|
||||
(RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"];
|
||||
- RCTAssert(accessibilityManager, @"Failed to get exported dimensions: AccessibilityManager is nil");
|
||||
+ // TOOD(T225745315): For some reason, accessibilityManager is nil in some cases.
|
||||
+ // We default the fontScale to 1.0 in this case. This should be okay: if we assume
|
||||
+ // that accessibilityManager will eventually become available, js will eventually
|
||||
+ // be updated with the correct fontScale.
|
||||
CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0;
|
||||
return RCTExportedDimensions(fontScale);
|
||||
}
|
||||
@@ -182,14 +218,7 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
|
||||
|
||||
- (NSDictionary<NSString *, id> *)getConstants
|
||||
{
|
||||
- return @{
|
||||
- @"Dimensions" : [self _exportedDimensions],
|
||||
- // Note:
|
||||
- // This prop is deprecated and will be removed in a future release.
|
||||
- // Please use this only for a quick and temporary solution.
|
||||
- // Use <SafeAreaView> instead.
|
||||
- @"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
|
||||
- };
|
||||
+ return _constants;
|
||||
}
|
||||
|
||||
- (void)didReceiveNewContentSizeMultiplier
|
||||
@@ -209,10 +238,11 @@ static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
|
||||
- (void)interfaceOrientationDidChange
|
||||
{
|
||||
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
||||
- UIWindow *keyWindow = RCTKeyWindow();
|
||||
- UIInterfaceOrientation nextOrientation = keyWindow.windowScene.interfaceOrientation;
|
||||
+ UIApplication *application = RCTSharedApplication();
|
||||
+ UIInterfaceOrientation nextOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
|
||||
|
||||
- BOOL isRunningInFullScreen = CGRectEqualToRect(keyWindow.frame, keyWindow.screen.bounds);
|
||||
+ BOOL isRunningInFullScreen =
|
||||
+ CGRectEqualToRect(application.delegate.window.frame, application.delegate.window.screen.bounds);
|
||||
// We are catching here two situations for multitasking view:
|
||||
// a) The app is in Split View and the container gets resized -> !isRunningInFullScreen
|
||||
// b) The app changes to/from fullscreen example: App runs in slide over mode and goes into fullscreen->
|
||||
@@ -276,3 +306,4 @@ Class RCTDeviceInfoCls(void)
|
||||
{
|
||||
return RCTDeviceInfo.class;
|
||||
}
|
||||
+
|
||||
diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
index cf14e51cf5f561b84f1b6ace8410fc77d626758e..abc8c64adf26fbf73429aee7fd4f76877e98849a 100644
|
||||
--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
+++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java
|
||||
@@ -42,6 +42,7 @@ import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
+import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -155,8 +156,15 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
}
|
||||
|
||||
private class ConcurrentOperationQueue {
|
||||
- private final Queue<UIThreadOperation> mQueue = new ConcurrentLinkedQueue<>();
|
||||
- @Nullable private UIThreadOperation mPeekedOperation = null;
|
||||
+ // Patch: Use LinkedBlockingQueue instead of ConcurrentLinkedQueue.
|
||||
+ // In some versions of Android, ConcurrentLinkedQueue is known to drop
|
||||
+ // items, causing crashing. See https://github.com/laurent22/joplin/issues/8425
|
||||
+ private final Queue<UIThreadOperation> mQueue = (
|
||||
+ // The issue exists for Android 12, which corresponds to API levels 31 and 32.
|
||||
+ Build.VERSION.SDK_INT == 31 || Build.VERSION.SDK_INT == 32
|
||||
+ ) ? new LinkedBlockingQueue<>() : new ConcurrentLinkedQueue<>();
|
||||
+
|
||||
+ @Nullable private UIThreadOperation mPeekedOperation = null;
|
||||
|
||||
@AnyThread
|
||||
boolean isEmpty() {
|
||||
@@ -1,21 +1,21 @@
|
||||
# This patch improves the note actions menu (the kebab menu)'s accessibility
|
||||
# by labelling its dismiss button.
|
||||
diff --git a/build/rnpm.js b/build/rnpm.js
|
||||
index 1111c2de99b3d4c5651ca4eee3ba59c0ce8e13e1..d410ee12b38d02c399b0a40973217da0082d73c0 100644
|
||||
index 47bc91a88b9e2246a0ce4295f9f932da6a572461..75b5a22bdcbc2594238bcf953df6d54e18cc7793 100644
|
||||
--- a/build/rnpm.js
|
||||
+++ b/build/rnpm.js
|
||||
@@ -1573,7 +1573,9 @@
|
||||
@@ -1267,7 +1267,9 @@
|
||||
onPress = _this$props.onPress,
|
||||
style = _this$props.style;
|
||||
return /*#__PURE__*/React__default.createElement(reactNative.TouchableWithoutFeedback, {
|
||||
return React__default.createElement(reactNative.TouchableWithoutFeedback, {
|
||||
- onPress: onPress
|
||||
+ onPress: onPress,
|
||||
+ accessibilityLabel: _this$props.accessibilityLabel,
|
||||
+ accessibilityRole: 'button',
|
||||
}, /*#__PURE__*/React__default.createElement(reactNative.Animated.View, {
|
||||
}, React__default.createElement(reactNative.Animated.View, {
|
||||
style: [styles.fullscreen, {
|
||||
opacity: this.fadeAnim
|
||||
@@ -1588,7 +1590,8 @@
|
||||
@@ -1282,7 +1284,8 @@
|
||||
}(React.Component);
|
||||
|
||||
Backdrop.propTypes = {
|
||||
@@ -25,24 +25,33 @@ index 1111c2de99b3d4c5651ca4eee3ba59c0ce8e13e1..d410ee12b38d02c399b0a40973217da0
|
||||
};
|
||||
var styles = reactNative.StyleSheet.create({
|
||||
fullscreen: {
|
||||
@@ -1658,6 +1661,7 @@
|
||||
@@ -1352,6 +1355,7 @@
|
||||
style: styles$1.placeholder
|
||||
}, /*#__PURE__*/React__default.createElement(Backdrop, {
|
||||
}, React__default.createElement(Backdrop, {
|
||||
onPress: ctx._onBackdropPress,
|
||||
+ accessibilityLabel: this.props.closeButtonLabel,
|
||||
style: backdropStyles,
|
||||
ref: ctx.onBackdropRef
|
||||
}), ctx._makeOptions());
|
||||
@@ -2090,6 +2094,7 @@
|
||||
}), /*#__PURE__*/React__default.createElement(MenuPlaceholder, {
|
||||
@@ -1784,6 +1788,7 @@
|
||||
}), React__default.createElement(MenuPlaceholder, {
|
||||
ctx: this,
|
||||
backdropStyles: customStyles.backdrop,
|
||||
+ closeButtonLabel: this.props.closeButtonLabel,
|
||||
ref: this._onPlaceholderRef
|
||||
}))));
|
||||
}
|
||||
@@ -1854,7 +1859,7 @@
|
||||
var _options$props = options.props,
|
||||
optionsContainerStyle = _options$props.optionsContainerStyle,
|
||||
renderOptionsContainer = _options$props.renderOptionsContainer,
|
||||
- customStyles = _options$props.customStyles;
|
||||
+ customStyles = _options$props.customStyles || {};
|
||||
var optionsRenderer = renderOptionsContainer || defaultOptionsContainerRenderer;
|
||||
var isOutside = !triggerLayout || !optionsLayout;
|
||||
|
||||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index 1db1e643a915e4bfb715e33354678ec1be219f50..007157e366d1935368bdd8eff5e7a0773e183d0f 100644
|
||||
index 7e1ef2e441a665e97c304984080399f9646395df..673c4f713757abfb1851cba0d4560020c83e5f50 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -18,6 +18,7 @@ declare module "react-native-popup-menu" {
|
||||
875
.yarn/releases/yarn-3.8.3.cjs
vendored
942
.yarn/releases/yarn-4.9.2.cjs
vendored
Executable file
11
.yarnrc.yml
@@ -2,17 +2,20 @@ nmHoistingLimits: workspaces
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
compressionLevel: mixed
|
||||
enableGlobalCache: false
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.8.3.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
logFilters:
|
||||
|
||||
# Disable useless non-actionable warnings.
|
||||
# https://github.com/yarnpkg/yarn/issues/4064
|
||||
|
||||
# e.g. "Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details."
|
||||
- code: YN0086
|
||||
level: discard
|
||||
|
||||
# eg "@joplin/app-desktop@workspace:packages/app-desktop provides react (p87edd) with version 18.2.0, which doesn't satisfy what @testing-library/react-hooks and some of its descendants request"
|
||||
- code: YN0060
|
||||
level: discard
|
||||
|
||||
@@ -1300,4 +1300,9 @@ footer .bottom-links-row p {
|
||||
|
||||
:lang(zh-cn) #plans-section .faq {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.cfa-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
BIN
Assets/WebsiteAssets/images/joplin_server_business/main.png
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/publish.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/self_host.jpg
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/share.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/teams.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/BestEtf.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/EssayService.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.1 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Freespinny.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/HomeworkGuy.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/PaperWriter.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/WritePaper.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -1,24 +1,28 @@
|
||||
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}}">
|
||||
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}} hosting-type-{{hostingType}}">
|
||||
<div class="price-container {{#featured}}price-container-blue{{/featured}}">
|
||||
<div class="price-row">
|
||||
<div class="plan-type">
|
||||
<img src="{{imageBaseUrl}}/{{iconName}}.png"/> {{title}}
|
||||
<div class="price-row">
|
||||
<div class="plan-type">
|
||||
<img src="{{imageBaseUrl}}/{{iconName}}.png"/> {{title}}
|
||||
</div>
|
||||
|
||||
{{#priceMonthly.formattedMonthlyAmount}}
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
{{/priceMonthly.formattedMonthlyAmount}}
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
{{#priceYearly.formattedMonthlyAmount}}
|
||||
<div class="plan-price-yearly-per-year">
|
||||
<div>
|
||||
({{priceYearly.formattedAmount}}<sub class="per-year"> <span translate>/year</span></sub>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plan-price-yearly-per-year">
|
||||
<div>
|
||||
({{priceYearly.formattedAmount}}<sub class="per-year"> <span translate>/year</span></sub>)
|
||||
</div>
|
||||
</div>
|
||||
{{/priceYearly.formattedMonthlyAmount}}
|
||||
|
||||
{{#featureLabelsOn}}
|
||||
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
|
||||
@@ -29,7 +33,11 @@
|
||||
{{/featureLabelsOff}}
|
||||
|
||||
<p class="text-center subscribe-wrapper">
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton cfa-button">{{cfaLabel}}</a>
|
||||
|
||||
{{#learnMoreUrl}}
|
||||
<a id="learnMore-{{name}}" href="{{learnMoreUrl}}" class="button-link btn-white learnMoreButton cfa-button">Learn more</a>
|
||||
{{/learnMoreUrl}}
|
||||
</p>
|
||||
|
||||
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
|
||||
|
||||
@@ -1,23 +1,91 @@
|
||||
<div id="plans-section" class="env-{{env}}">
|
||||
<style>
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
border: 2px solid black;
|
||||
border-radius: 100px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.toggle-option {
|
||||
flex: 1;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toggle-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Cloud <span class="frame-bg frame-bg-yellow">plans</span>
|
||||
Our synchronisation and sharing <span class="frame-bg frame-bg-yellow">solutions</span>
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
<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.
|
||||
Synchronise and share your notes with our range of plans.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-container" id="toggle">
|
||||
<div class="toggle-option active toggle-button-managed">Managed hosting</div>
|
||||
<div class="toggle-option inactive toggle-button-self">Self-hosting</div>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div class="alert alert-danger alert-env-dev" role="alert" style='text-align: center; margin-top: 10px;'>
|
||||
To use this page please enable JavaScript!
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 1.2em">
|
||||
<div class="row hosting-type-managed">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Cloud
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row hosting-type-self">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Server Business
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 1.2em" class="hosting-type-managed">
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="pay-monthly-radio" class="form-check-input" type="radio" name="pay-radio" checked value="monthly">
|
||||
<label translate style="font-weight: bold" class="form-check-label" for="pay-monthly-radio">
|
||||
@@ -46,7 +114,11 @@
|
||||
{{> plan}}
|
||||
{{/plans.teams}}
|
||||
|
||||
<p translate class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
{{#plans.joplinServerBusiness}}
|
||||
{{> plan}}
|
||||
{{/plans.joplinServerBusiness}}
|
||||
|
||||
<p translate class="joplin-cloud-login-info hosting-type-managed">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -148,4 +220,30 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
const setHostingType = (type) => {
|
||||
const other = type === 'managed' ? 'self' : 'managed';
|
||||
$('.toggle-button-' + type).addClass('active');
|
||||
$('.toggle-button-' + type).removeClass('inactive');
|
||||
$('.toggle-button-' + other).addClass('inactive');
|
||||
$('.toggle-button-' + other).removeClass('active');
|
||||
|
||||
$('.hosting-type-' + type).show();
|
||||
$('.hosting-type-' + other).hide();
|
||||
}
|
||||
|
||||
$('.toggle-button-managed').click((event) => {
|
||||
event.preventDefault();
|
||||
setHostingType('managed');
|
||||
});
|
||||
|
||||
$('.toggle-button-self').click((event) => {
|
||||
event.preventDefault();
|
||||
setHostingType('self');
|
||||
});
|
||||
|
||||
setHostingType('managed');
|
||||
</script>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ RUN corepack enable
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY .yarn/plugins ./.yarn/plugins
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
COPY .yarn/patches ./.yarn/patches
|
||||
COPY package.json .
|
||||
|
||||
51
Dockerfile.transcribe
Normal file
@@ -0,0 +1,51 @@
|
||||
FROM node:18-bullseye
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
ca-certificates curl \
|
||||
python3 tini
|
||||
|
||||
## install docker
|
||||
RUN install -m 0755 -d /etc/apt/keyrings
|
||||
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
RUN chmod a+r /etc/apt/keyrings/docker.asc
|
||||
RUN echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
|
||||
$(. /etc/os-release && echo bullseye) stable" | \
|
||||
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY .yarn/plugins ./.yarn/plugins
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
COPY .yarn/patches ./.yarn/patches
|
||||
COPY package.json .
|
||||
COPY .yarnrc.yml .
|
||||
COPY yarn.lock .
|
||||
COPY gulpfile.js .
|
||||
COPY tsconfig.json .
|
||||
COPY packages/lib ./packages/lib
|
||||
COPY packages/utils ./packages/utils
|
||||
COPY packages/tools ./packages/tools
|
||||
COPY packages/renderer ./packages/renderer
|
||||
COPY packages/htmlpack ./packages/htmlpack
|
||||
COPY packages/transcribe ./packages/transcribe
|
||||
|
||||
# We don't want to build onenote-converter since it is not used by the server
|
||||
RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
|
||||
|
||||
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
|
||||
&& yarn cache clean \
|
||||
&& rm -rf .yarn/berry
|
||||
|
||||
WORKDIR /app/packages/transcribe
|
||||
|
||||
# Start the Node.js application
|
||||
CMD ["yarn", "start"]
|
||||
@@ -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://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.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://www.reddit.com/r/tiktokRise/"><img title="Tiktok Rise" width="256" src="https://joplinapp.org/images/sponsors/TiktokRise.jpg" alt="Tiktok Rise"/></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://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>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
||||
10
devbox.json
@@ -9,24 +9,20 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "latest",
|
||||
"nodejs": "23.8.0",
|
||||
"pkg-config": "latest",
|
||||
"pixman": "latest",
|
||||
"cairo.dev": "",
|
||||
"pango.dev": "",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
"version": "",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"python": "latest",
|
||||
"python": "3.13.1",
|
||||
"bat": "latest",
|
||||
"electron": {
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "latest",
|
||||
"giflib": "latest",
|
||||
"git": "2.47.2",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
||||
@@ -21,7 +21,7 @@ version: '2'
|
||||
services:
|
||||
|
||||
postgresql-master:
|
||||
image: 'bitnami/postgresql:16.3.0'
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||
|
||||
postgresql-slave:
|
||||
image: 'bitnami/postgresql:16.3.0'
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
ports:
|
||||
- '5433:5432'
|
||||
depends_on:
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"packages/renderer/MdToHtml/rules/fence.js": true,
|
||||
"packages/renderer/MdToHtml/rules/mermaid.js": true,
|
||||
"packages/renderer/MdToHtml/rules/sanitize_html.js": true,
|
||||
"packages/transcribe/dist": true,
|
||||
"packages/server/db-*.sqlite": true,
|
||||
"packages/server/dist/": true,
|
||||
"packages/utils/dist/": true,
|
||||
|
||||
37
package.json
@@ -10,14 +10,14 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"yarn": "3.8.3"
|
||||
"yarn": "4.9.2"
|
||||
},
|
||||
"scripts": {
|
||||
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildScriptIndexes": "node packages/tools/gulp/tasks/buildScriptIndexesRun.js",
|
||||
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn tsc",
|
||||
"buildParallel": "yarn workspaces foreach --worktree --verbose --interlaced --parallel --jobs 2 --topological-dev run build && yarn tsc",
|
||||
"buildPluginDoc": "cd packages/generate-plugin-doc && yarn buildPluginDoc_",
|
||||
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn tsc",
|
||||
"buildSequential": "yarn workspaces foreach --worktree --verbose --interlaced --topological-dev run build && yarn tsc",
|
||||
"buildServerDocker": "node packages/tools/buildServerDocker.js",
|
||||
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
|
||||
"buildTranslations": "node packages/tools/build-translation.js",
|
||||
@@ -38,6 +38,7 @@
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
|
||||
"syncFuzzer": "node ./packages/tools/fuzzer/sync-fuzzer.js",
|
||||
"postinstall": "husky && gulp build",
|
||||
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
|
||||
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
@@ -53,19 +54,19 @@
|
||||
"setupNewRelease": "node ./packages/tools/setupNewRelease",
|
||||
"spellcheck": "node packages/tools/spellcheck.js",
|
||||
"tagServerLatest": "node packages/tools/tagServerLatest.js",
|
||||
"test-ci": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test-ci",
|
||||
"test": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test",
|
||||
"tsc": "yarn workspaces foreach --parallel --verbose --interlaced run tsc",
|
||||
"test-ci": "yarn workspaces foreach --worktree --parallel --verbose --interlaced --jobs 2 run test-ci",
|
||||
"test": "yarn workspaces foreach --worktree --parallel --verbose --interlaced --jobs 2 run test",
|
||||
"tsc": "yarn workspaces foreach --worktree --parallel --verbose --interlaced run tsc",
|
||||
"updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
|
||||
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
|
||||
"updateNews": "node ./packages/tools/website/updateNews",
|
||||
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
|
||||
"validateFilenames": "node ./packages/tools/validateFilenames.js",
|
||||
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
|
||||
"watch": "yarn workspaces foreach --worktree --parallel --verbose --interlaced --jobs 999 run watch",
|
||||
"watchWebsite": "nodemon --delay 1 --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --watch packages/doc-builder/build --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crowdin/cli": "3",
|
||||
"@crowdin/cli": "4",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
@@ -79,13 +80,13 @@
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.4.5",
|
||||
"glob": "11.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.2.8",
|
||||
"madge": "7.0.0",
|
||||
"npm-package-json-lint": "7.1.0",
|
||||
"lint-staged": "15.5.0",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -93,9 +94,9 @@
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.4.1",
|
||||
"nodemon": "3.1.7"
|
||||
"nodemon": "3.1.9"
|
||||
},
|
||||
"packageManager": "yarn@3.8.3",
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
@@ -104,12 +105,16 @@
|
||||
"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",
|
||||
"chokidar@^2.0.0": "3.5.3",
|
||||
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
|
||||
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
|
||||
"app-builder-lib@26.0.0-alpha.7": "patch:app-builder-lib@npm%3A26.0.0-alpha.7#./.yarn/patches/app-builder-lib-npm-26.0.0-alpha.7-e1b3dca119.patch",
|
||||
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
|
||||
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
|
||||
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
|
||||
"react-native-popup-menu@0.16.1": "patch:react-native-popup-menu@npm%3A0.16.1#./.yarn/patches/react-native-popup-menu-npm-0.16.1-28fd66ecb5.patch"
|
||||
"react-native-popup-menu@0.17.0": "patch:react-native-popup-menu@npm%3A0.17.0#./.yarn/patches/react-native-popup-menu-npm-0.17.0-8b745d88dd.patch",
|
||||
"react-native@0.79.2": "patch:react-native@npm%3A0.79.2#./.yarn/patches/react-native-npm-0.79.2-9db13eddfe.patch",
|
||||
"pdfjs-dist@2.16.105": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import { reg } from '@joplin/lib/registry.js';
|
||||
import { fileExtension } from '@joplin/lib/path-utils';
|
||||
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';
|
||||
@@ -16,6 +16,7 @@ import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import setupCommand from './setupCommand';
|
||||
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');
|
||||
@@ -76,6 +77,12 @@ class Application extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
public async loadItemOrFail(type: ModelType | 'folderOrNote', pattern: string) {
|
||||
const output = await this.loadItem(type, pattern);
|
||||
if (!output) throw new Error(_('Cannot find "%s".', pattern));
|
||||
return output;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async loadItems(type: ModelType | 'folderOrNote', pattern: string, options: any = null): Promise<(FolderEntity | NoteEntity)[]> {
|
||||
if (type === 'folderOrNote') {
|
||||
@@ -397,8 +404,12 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
public async start(argv: string[]) {
|
||||
const keychainEnabled = this.checkIfKeychainEnabled(argv);
|
||||
// TODO: Currently, `pluginAssetDir` needs to be set differently for each platform and requires
|
||||
// a call to Setting.setConstant. Ideally, this would be done in a way that requires users to
|
||||
// set this constant on startup.
|
||||
Setting.setConstant('pluginAssetDir', `${dirname(require.resolve('@joplin/renderer'))}/assets`);
|
||||
|
||||
const keychainEnabled = this.checkIfKeychainEnabled(argv);
|
||||
argv = await super.start(argv, { keychainEnabled });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -408,8 +419,15 @@ class Application extends BaseApplication {
|
||||
|
||||
this.initRedux();
|
||||
|
||||
// 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();
|
||||
|
||||
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
|
||||
|
||||
initializeCommandService(this.store(), Setting.value('env') === Env.Dev);
|
||||
|
||||
// If we have some arguments left at this point, it's a command
|
||||
// so execute it.
|
||||
if (argv.length) {
|
||||
@@ -448,11 +466,6 @@ class Application extends BaseApplication {
|
||||
this.gui_.setLogger(this.logger());
|
||||
await this.gui_.start();
|
||||
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting.dispatchUpdateAll();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
await refreshFolders((action: any) => this.store().dispatch(action), '');
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('command-done', () => {
|
||||
});
|
||||
|
||||
it('should make a note as "done"', async () => {
|
||||
const note = await Note.save({ title: 'hello', is_todo: 1, todo_completed: 0 });
|
||||
const note = await Note.save({ title: 'hello', is_todo: 1, todo_completed: 0, parent_id: '' });
|
||||
|
||||
const command = setupCommandForTesting(Command);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class Command extends BaseCommand {
|
||||
['-v, --verbose', 'More verbose output for the `target-status` command'],
|
||||
['-o, --output <directory>', 'Output directory'],
|
||||
['--retry-failed-items', 'Applies to `decrypt` command - retries decrypting items that previously could not be decrypted.'],
|
||||
['-f, --force', 'Do not ask for input on failure'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ class Command extends BaseCommand {
|
||||
this.stdout(line.join('\n'));
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error.code === 'masterKeyNotLoaded') {
|
||||
if (error.code === 'masterKeyNotLoaded' && !args.options.force) {
|
||||
const ok = await askForMasterKey(error);
|
||||
if (!ok) return;
|
||||
continue;
|
||||
|
||||
@@ -26,8 +26,7 @@ class Command extends BaseCommand {
|
||||
const pattern = args['notebook'];
|
||||
const force = args.options && args.options.force === true;
|
||||
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const folder = await app().loadItemOrFail(BaseModel.TYPE_FOLDER, pattern);
|
||||
|
||||
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
||||
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
||||
|
||||
179
packages/app-cli/app/command-share.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import mockShareService, { ApiMock } from '@joplin/lib/testing/share/mockShareService';
|
||||
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { ShareInvitation, ShareUserStatus, StateShare } from '@joplin/lib/services/share/reducer';
|
||||
import app from './app';
|
||||
const Command = require('./command-share');
|
||||
|
||||
const setUpCommand = () => {
|
||||
const output: string[] = [];
|
||||
const stdout = (content: string) => {
|
||||
output.push(...content.split('\n'));
|
||||
};
|
||||
|
||||
const command = setupCommandForTesting(Command, stdout);
|
||||
return { command, output };
|
||||
};
|
||||
|
||||
const shareId = 'test-id';
|
||||
const defaultFolderShare: StateShare = {
|
||||
id: shareId,
|
||||
type: ModelType.Folder,
|
||||
folder_id: 'some-folder-id-here',
|
||||
note_id: undefined,
|
||||
master_key_id: undefined,
|
||||
user: {
|
||||
full_name: 'Test user',
|
||||
email: 'test@localhost',
|
||||
id: 'some-user-id',
|
||||
},
|
||||
};
|
||||
|
||||
const mockShareServiceForFolderSharing = (eventHandlerOverrides: Partial<ApiMock>&{ onExec?: undefined }) => {
|
||||
const invitations: ShareInvitation[] = [];
|
||||
|
||||
mockShareService({
|
||||
getShareInvitations: async () => ({
|
||||
items: invitations,
|
||||
}),
|
||||
getShares: async () => ({ items: [defaultFolderShare] }),
|
||||
getShareUsers: async (_id: string) => ({ items: [] }),
|
||||
postShareUsers: async (_id, _body) => { },
|
||||
postShares: async () => ({ id: shareId }),
|
||||
...eventHandlerOverrides,
|
||||
}, ShareService.instance(), app().store());
|
||||
|
||||
return {
|
||||
addInvitation: (invitation: Partial<ShareInvitation>) => {
|
||||
const defaultInvitation: ShareInvitation = {
|
||||
share: defaultFolderShare,
|
||||
id: 'some-invitation-id',
|
||||
master_key: undefined,
|
||||
status: ShareUserStatus.Waiting,
|
||||
can_read: 1,
|
||||
can_write: 1,
|
||||
};
|
||||
|
||||
invitations.push({ ...defaultInvitation, ...invitation });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
describe('command-share', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
await setupApplication();
|
||||
BaseItem.shareService_ = ShareService.instance();
|
||||
});
|
||||
|
||||
test('should allow adding a user to a share', async () => {
|
||||
const folder = await Folder.save({ title: 'folder1' });
|
||||
|
||||
let lastShareUserUpdate: unknown|null = null;
|
||||
mockShareServiceForFolderSharing({
|
||||
getShares: async () => {
|
||||
const isShared = !!lastShareUserUpdate;
|
||||
if (isShared) {
|
||||
return {
|
||||
items: [{ ...defaultFolderShare, folder_id: folder.id }],
|
||||
};
|
||||
} else {
|
||||
return { items: [] };
|
||||
}
|
||||
},
|
||||
// Called when a new user is added to a share
|
||||
postShareUsers: async (_id, body) => {
|
||||
lastShareUserUpdate = body;
|
||||
},
|
||||
});
|
||||
|
||||
const { command } = setUpCommand();
|
||||
|
||||
// Should share read-write by default
|
||||
await command.action({
|
||||
'command': 'add',
|
||||
'notebook': 'folder1',
|
||||
'user': 'test@localhost',
|
||||
options: {},
|
||||
});
|
||||
expect(lastShareUserUpdate).toMatchObject({
|
||||
email: 'test@localhost',
|
||||
can_write: 1,
|
||||
can_read: 1,
|
||||
});
|
||||
|
||||
// Should also support sharing as read only
|
||||
await command.action({
|
||||
'command': 'add',
|
||||
'notebook': 'folder1',
|
||||
'user': 'test2@localhost',
|
||||
options: {
|
||||
'read-only': true,
|
||||
},
|
||||
});
|
||||
expect(lastShareUserUpdate).toMatchObject({
|
||||
email: 'test2@localhost',
|
||||
can_write: 0,
|
||||
can_read: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
label: 'should list a single pending invitation',
|
||||
invitations: [{ id: 'test', status: ShareUserStatus.Waiting }],
|
||||
expectedOutput: [
|
||||
'Incoming shares:',
|
||||
'\tWaiting: Notebook some-folder-id-here from test@localhost',
|
||||
'All shared folders:',
|
||||
'\tNone',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
label: 'should list accepted invitations for non-existent folders with [None] as the folder title',
|
||||
invitations: [
|
||||
{ id: 'test2', status: ShareUserStatus.Accepted },
|
||||
],
|
||||
expectedOutput: [
|
||||
'Incoming shares:',
|
||||
'\tAccepted: Notebook [None] from test@localhost',
|
||||
'All shared folders:',
|
||||
'\tNone',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
label: 'should not list rejected shares',
|
||||
invitations: [
|
||||
{ id: 'test3', status: ShareUserStatus.Rejected },
|
||||
],
|
||||
expectedOutput: [
|
||||
'Incoming shares:',
|
||||
'\tNone',
|
||||
'All shared folders:',
|
||||
'\tNone',
|
||||
].join('\n'),
|
||||
},
|
||||
])('share invitations: $label', async ({ invitations, expectedOutput }) => {
|
||||
const mock = mockShareServiceForFolderSharing({});
|
||||
for (const invitation of invitations) {
|
||||
mock.addInvitation(invitation);
|
||||
}
|
||||
|
||||
await ShareService.instance().refreshShareInvitations();
|
||||
|
||||
const { command, output } = setUpCommand();
|
||||
await command.action({
|
||||
'command': 'list',
|
||||
options: {},
|
||||
});
|
||||
|
||||
expect(output.join('\n')).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
297
packages/app-cli/app/command-share.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { ShareUserStatus } from '@joplin/lib/services/share/reducer';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import invitationRespond from '@joplin/lib/services/share/invitationRespond';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
|
||||
const logger = Logger.create('command-share');
|
||||
|
||||
type Args = {
|
||||
command: string;
|
||||
// eslint-disable-next-line id-denylist -- The "notebook" identifier comes from the UI.
|
||||
notebook?: string;
|
||||
user?: string;
|
||||
options: {
|
||||
'read-only'?: boolean;
|
||||
json?: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const folderTitle = (folder: FolderEntity|null) => {
|
||||
return folder ? substrWithEllipsis(folder.title, 0, 32) : _('[None]');
|
||||
};
|
||||
|
||||
const getShareState = () => app().store().getState().shareService;
|
||||
const getShareFromFolderId = (folderId: string) => {
|
||||
const shareState = getShareState();
|
||||
const allShares = shareState.shares;
|
||||
const share = allShares.find(share => share.folder_id === folderId);
|
||||
return share;
|
||||
};
|
||||
|
||||
const getShareUsers = (folderId: string) => {
|
||||
const share = getShareFromFolderId(folderId);
|
||||
if (!share) {
|
||||
throw new Error(`No share found for folder ${folderId}`);
|
||||
}
|
||||
return getShareState().shareUsers[share.id];
|
||||
};
|
||||
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
return 'share <command> [notebook] [user]';
|
||||
}
|
||||
|
||||
public description() {
|
||||
return [
|
||||
_('Shares or unshares the specified [notebook] with [user]. Requires Joplin Cloud or Joplin Server.'),
|
||||
_('Commands: `add`, `remove`, `list`, `delete`, `accept`, `leave`, and `reject`.'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
public options() {
|
||||
return [
|
||||
['--read-only', _('Don\'t allow the share recipient to write to the shared notebook. Valid only for the `add` subcommand.')],
|
||||
['-f, --force', _('Do not ask for user confirmation.')],
|
||||
['--json', _('Prefer JSON output.')],
|
||||
];
|
||||
}
|
||||
|
||||
public async action(args: Args) {
|
||||
const commandShareAdd = async (folder: FolderEntity, email: string) => {
|
||||
await reg.waitForSyncFinishedThenSync();
|
||||
|
||||
const share = await ShareService.instance().shareFolder(folder.id);
|
||||
|
||||
const permissions = {
|
||||
can_read: 1,
|
||||
can_write: args.options['read-only'] ? 0 : 1,
|
||||
};
|
||||
logger.debug('Sharing folder', folder.id, 'with', email, 'permissions=', permissions);
|
||||
|
||||
await ShareService.instance().addShareRecipient(share.id, share.master_key_id, email, permissions);
|
||||
|
||||
await ShareService.instance().refreshShares();
|
||||
await ShareService.instance().refreshShareUsers(share.id);
|
||||
|
||||
await reg.waitForSyncFinishedThenSync();
|
||||
};
|
||||
|
||||
const commandShareRemove = async (folder: FolderEntity, email: string) => {
|
||||
await ShareService.instance().refreshShares();
|
||||
|
||||
const share = getShareFromFolderId(folder.id);
|
||||
if (!share) {
|
||||
throw new Error(`No share found for folder ${folder.id}`);
|
||||
}
|
||||
|
||||
await ShareService.instance().refreshShareUsers(share.id);
|
||||
|
||||
const shareUsers = getShareUsers(folder.id);
|
||||
if (!shareUsers) {
|
||||
throw new Error(`No share found for folder ${folder.id}`);
|
||||
}
|
||||
|
||||
const targetUser = shareUsers.find(user => user.user?.email === email);
|
||||
if (!targetUser) {
|
||||
throw new Error(`No recipient found with email ${email}`);
|
||||
}
|
||||
|
||||
await ShareService.instance().deleteShareRecipient(targetUser.id);
|
||||
this.stdout(_('Removed %s from share.', targetUser.user.email));
|
||||
};
|
||||
|
||||
const commandShareList = async () => {
|
||||
let folder = null;
|
||||
if (args.notebook) {
|
||||
folder = await app().loadItemOrFail(ModelType.Folder, args.notebook);
|
||||
}
|
||||
|
||||
await ShareService.instance().maintenance();
|
||||
|
||||
if (folder) {
|
||||
const share = getShareFromFolderId(folder.id);
|
||||
await ShareService.instance().refreshShareUsers(share.id);
|
||||
|
||||
const shareUsers = getShareUsers(folder.id);
|
||||
const output = {
|
||||
folderTitle: folderTitle(folder),
|
||||
sharedWith: (shareUsers ?? []).map(user => ({
|
||||
email: user.user.email,
|
||||
readOnly: user.can_read && !user.can_write,
|
||||
})),
|
||||
};
|
||||
|
||||
if (args.options.json) {
|
||||
this.stdout(JSON.stringify(output));
|
||||
} else {
|
||||
this.stdout(_('Folder "%s" is shared with:', output.folderTitle));
|
||||
for (const user of output.sharedWith) {
|
||||
this.stdout(`\t${user.email}\t${user.readOnly ? _('(Read-only)') : ''}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const shareState = getShareState();
|
||||
const output = {
|
||||
invitations: shareState.shareInvitations.map(invitation => ({
|
||||
accepted: invitation.status === ShareUserStatus.Accepted,
|
||||
waiting: invitation.status === ShareUserStatus.Waiting,
|
||||
rejected: invitation.status === ShareUserStatus.Rejected,
|
||||
folderId: invitation.share.folder_id,
|
||||
fromUser: {
|
||||
email: invitation.share.user?.email,
|
||||
},
|
||||
})),
|
||||
shares: shareState.shares.map(share => ({
|
||||
isFolder: !!share.folder_id,
|
||||
isNote: !!share.note_id,
|
||||
itemId: share.folder_id ?? share.note_id,
|
||||
fromUser: {
|
||||
email: share.user?.email,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
if (args.options.json) {
|
||||
this.stdout(JSON.stringify(output));
|
||||
} else {
|
||||
this.stdout(_('Incoming shares:'));
|
||||
let loggedInvitation = false;
|
||||
for (const invitation of output.invitations) {
|
||||
let message;
|
||||
if (invitation.waiting) {
|
||||
message = _('Waiting: Notebook %s from %s', invitation.folderId, invitation.fromUser.email);
|
||||
}
|
||||
if (invitation.accepted) {
|
||||
const folder = await Folder.load(invitation.folderId);
|
||||
message = _('Accepted: Notebook %s from %s', folderTitle(folder), invitation.fromUser.email);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
this.stdout(`\t${message}`);
|
||||
loggedInvitation = true;
|
||||
}
|
||||
}
|
||||
if (!loggedInvitation) {
|
||||
this.stdout(`\t${_('None')}`);
|
||||
}
|
||||
|
||||
this.stdout(_('All shared folders:'));
|
||||
if (output.shares.length) {
|
||||
for (const share of output.shares) {
|
||||
let title;
|
||||
if (share.isFolder) {
|
||||
title = folderTitle(await Folder.load(share.itemId));
|
||||
} else {
|
||||
title = share.itemId;
|
||||
}
|
||||
|
||||
if (share.fromUser?.email) {
|
||||
this.stdout(`\t${_('%s from %s', title, share.fromUser?.email)}`);
|
||||
} else {
|
||||
this.stdout(`\t${title} - ${share.itemId}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.stdout(`\t${_('None')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const commandShareAcceptOrReject = async (folderId: string, accept: boolean) => {
|
||||
await ShareService.instance().maintenance();
|
||||
|
||||
const shareState = getShareState();
|
||||
const invitations = shareState.shareInvitations.filter(invitation => {
|
||||
return invitation.share.folder_id === folderId && invitation.status === ShareUserStatus.Waiting;
|
||||
});
|
||||
if (invitations.length === 0) throw new Error('No such invitation found');
|
||||
|
||||
// If there are multiple invitations for the same folder, stop early to avoid
|
||||
// accepting the wrong invitation.
|
||||
if (invitations.length > 1) throw new Error('Multiple invitations found with the same ID');
|
||||
|
||||
const invitation = invitations[0];
|
||||
|
||||
this.stdout(accept ? _('Accepting share...') : _('Rejecting share...'));
|
||||
await invitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, accept);
|
||||
};
|
||||
|
||||
const commandShareAccept = (folderId: string) => (
|
||||
commandShareAcceptOrReject(folderId, true)
|
||||
);
|
||||
|
||||
const commandShareReject = (folderId: string) => (
|
||||
commandShareAcceptOrReject(folderId, false)
|
||||
);
|
||||
|
||||
const commandShareDelete = async (folder: FolderEntity) => {
|
||||
const force = args.options.force;
|
||||
const ok = force ? true : await this.prompt(
|
||||
_('Unshare notebook "%s"? This may cause other users to lose access to the notebook.', folderTitle(folder)),
|
||||
{ booleanAnswerDefault: 'n' },
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
logger.info('Unsharing folder', folder.id);
|
||||
await ShareService.instance().unshareFolder(folder.id);
|
||||
await reg.scheduleSync();
|
||||
};
|
||||
|
||||
if (args.command === 'add' || args.command === 'remove' || args.command === 'delete') {
|
||||
if (!args.notebook) throw new Error('[notebook] is required');
|
||||
const folder = await app().loadItemOrFail(ModelType.Folder, args.notebook);
|
||||
|
||||
if (args.command === 'delete') {
|
||||
return commandShareDelete(folder);
|
||||
} else {
|
||||
if (!args.user) throw new Error('[user] is required');
|
||||
|
||||
const email = args.user;
|
||||
if (args.command === 'add') {
|
||||
return commandShareAdd(folder, email);
|
||||
} else if (args.command === 'remove') {
|
||||
return commandShareRemove(folder, email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.command === 'leave') {
|
||||
const folder = args.notebook ? await app().loadItemOrFail(ModelType.Folder, args.notebook) : null;
|
||||
|
||||
await ShareService.instance().maintenance();
|
||||
|
||||
return CommandService.instance().execute(
|
||||
'leaveSharedFolder', folder?.id, { force: args.options.force },
|
||||
);
|
||||
}
|
||||
|
||||
if (args.command === 'list') {
|
||||
return commandShareList();
|
||||
}
|
||||
|
||||
if (args.command === 'accept') {
|
||||
return commandShareAccept(args.notebook);
|
||||
}
|
||||
|
||||
if (args.command === 'reject') {
|
||||
return commandShareReject(args.notebook);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown subcommand: ${args.command}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
@@ -17,6 +17,7 @@ import { pathExists, writeFile } from 'fs-extra';
|
||||
import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
|
||||
const logger = Logger.create('command-sync');
|
||||
|
||||
@@ -230,6 +231,10 @@ class Command extends BaseCommand {
|
||||
return cleanUp();
|
||||
}
|
||||
|
||||
// Refresh share invitations -- if running without a GUI, some of the
|
||||
// maintenance tasks may otherwise be skipped.
|
||||
await ShareService.instance().maintenance();
|
||||
|
||||
this.stdout(_('Starting synchronisation...'));
|
||||
|
||||
const contextKey = `sync.${this.syncTargetId_}.context`;
|
||||
|
||||
@@ -4,7 +4,7 @@ import Note from '@joplin/lib/models/Note';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
|
||||
import { readCredentialFile } from '@joplin/lib/utils/credentialFiles';
|
||||
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
|
||||
import JoplinServerApi, { Session } from '@joplin/lib/JoplinServerApi';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function randomElement(array: any[]): any {
|
||||
@@ -107,6 +107,7 @@ class Command extends BaseCommand {
|
||||
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
|
||||
username: () => joplinServerAuth.email,
|
||||
password: () => joplinServerAuth.password,
|
||||
session: (): Session => null,
|
||||
});
|
||||
|
||||
const apiPut = async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const Revision = require('@joplin/lib/models/Revision').default;
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
const shimInitCli = require('./utils/shimInitCli').default;
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
|
||||
@@ -73,7 +73,7 @@ function appVersion() {
|
||||
return p.version;
|
||||
}
|
||||
|
||||
shimInit({ sharp, keytar, appVersion, nodeSqlite });
|
||||
shimInitCli({ sharp, keytar, appVersion, nodeSqlite });
|
||||
|
||||
const logger = new Logger();
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
|
||||
14
packages/app-cli/app/utils/initializeCommandService.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
||||
import libCommands from '@joplin/lib/commands/index';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { Store } from 'redux';
|
||||
|
||||
export default function initializeCommandService(store: Store<State>, devMode: boolean) {
|
||||
CommandService.instance().initialize(store, devMode, stateToWhenClauseContext);
|
||||
|
||||
for (const command of libCommands) {
|
||||
CommandService.instance().registerDeclaration(command.declaration);
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime());
|
||||
}
|
||||
}
|
||||
32
packages/app-cli/app/utils/shimInitCli.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import shim, { ShowMessageBoxOptions } from '@joplin/lib/shim';
|
||||
import type { ShimInitOptions } from '@joplin/lib/shim-init-node';
|
||||
import app from '../app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
|
||||
const shimInitCli = (options: ShimInitOptions) => {
|
||||
shimInit(options);
|
||||
|
||||
shim.showMessageBox = async (message: string, options: ShowMessageBoxOptions) => {
|
||||
const gui = app()?.gui();
|
||||
let answers = options.buttons ?? [_('Ok'), _('Cancel')];
|
||||
|
||||
if (options.type === 'error' || options.type === 'info') {
|
||||
answers = [];
|
||||
}
|
||||
|
||||
message += answers.length ? `(${answers.join(', ')})` : '';
|
||||
|
||||
const answer = await gui.prompt(options.title ?? '', `${message} `, { answers });
|
||||
|
||||
if (answers.includes(answer)) {
|
||||
return answers.indexOf(answer);
|
||||
} else if (answer) {
|
||||
return answers.findIndex(a => a.startsWith(answer));
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
};
|
||||
|
||||
export default shimInitCli;
|
||||
@@ -15,4 +15,7 @@ export const setupApplication = async () => {
|
||||
// such notebook.
|
||||
await Folder.save({ title: 'default' });
|
||||
await app().refreshCurrentFolder();
|
||||
|
||||
// Some tests also need access to the Redux store
|
||||
app().initRedux();
|
||||
};
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.3.1",
|
||||
"version": "3.4.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.3",
|
||||
"@joplin/renderer": "~3.3",
|
||||
"@joplin/utils": "~3.3",
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -55,24 +55,25 @@
|
||||
"node-rsa": "1.1.1",
|
||||
"open": "8.4.2",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.33.4",
|
||||
"sharp": "0.33.5",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"terminal-kit": "3.1.1",
|
||||
"terminal-kit": "3.1.2",
|
||||
"tkwidgets": "0.5.27",
|
||||
"url-parse": "1.5.10",
|
||||
"word-wrap": "1.2.5",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.3",
|
||||
"@joplin/tools": "~3.4",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.19.67",
|
||||
"@types/node": "18.19.86",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
||||
1
packages/app-cli/tests/html_to_md/comments_in_style.html
Normal file
@@ -0,0 +1 @@
|
||||
<p><span style="/* Comment */ text-decoration: underline;">Test</span>. In the past, <span style="font-size: auto;/* Test! */">comments</span> in CSS have caused issues.</p>
|
||||
1
packages/app-cli/tests/html_to_md/comments_in_style.md
Normal file
@@ -0,0 +1 @@
|
||||
<ins>Test</ins>. In the past, comments in CSS have caused issues.
|
||||
@@ -0,0 +1 @@
|
||||
<p>Some **format** characters $need$ to be `escaped`, if the characters were included directly in HTML.</p>
|
||||
@@ -0,0 +1 @@
|
||||
Some \*\*format\*\* characters \$need\$ to be \`escaped\`, if the characters were included directly in HTML.
|
||||
@@ -351,7 +351,7 @@ describe('services_PluginService', () => {
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
const dataDir = await joplin.plugins.dataDir();
|
||||
joplin.data.post(['folders'], null, { title: JSON.stringify(dataDir) });
|
||||
await joplin.data.post(['folders'], null, { title: JSON.stringify(dataDir) });
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<body>
|
||||
<a name="519"/>
|
||||
<h1>Second note</h1>
|
||||
</body>
|
||||
@@ -0,0 +1,2 @@
|
||||
<img src="..\\..\\photo.jpg" />
|
||||
<a href="..\\..\\sample.txt">Sample</a>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
|
||||
19
packages/app-desktop/.eslintrc.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
'overrides': [
|
||||
{
|
||||
files: ['**/*.tsx', '**/*.js', '**/*.ts'],
|
||||
rules: {
|
||||
'no-restricted-globals': ['error',
|
||||
...['alert', 'confirm', 'prompt'].map(alertLikeFunction => ({
|
||||
'name': alertLikeFunction,
|
||||
'message': [
|
||||
'Avoid using alert()/confirm()/prompt() in the desktop app -- they break keyboard input on some systems.',
|
||||
'Prefer shim.showMessageBox and shim.showConfirmationDialog.',
|
||||
'See https://github.com/electron/electron/issues/19977.',
|
||||
].join(' '),
|
||||
})),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
4
packages/app-desktop/.gitignore
vendored
@@ -25,3 +25,7 @@ build/7zip/7za
|
||||
build/7zip/7za.exe
|
||||
sentry.properties
|
||||
downloads/
|
||||
|
||||
# Bundler output
|
||||
*.js.meta.json
|
||||
*.bundle.js
|
||||
|
||||
@@ -6,7 +6,7 @@ const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
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 } from 'electron';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
|
||||
import bridge from './bridge';
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
@@ -75,6 +75,7 @@ export default class ElectronAppWrapper {
|
||||
private ipcStartPort_ = 2658;
|
||||
|
||||
private ipcLogger_: Logger;
|
||||
private ipcLoggerFilePath_: string;
|
||||
|
||||
public constructor(electronApp: App, { env, profilePath, isDebugMode, initialCallbackUrl, isEndToEndTesting }: Options) {
|
||||
this.electronApp_ = electronApp;
|
||||
@@ -90,8 +91,9 @@ export default class ElectronAppWrapper {
|
||||
// calls, either because it hasn't been set or other issue. So we set one here specifically
|
||||
// for this.
|
||||
this.ipcLogger_ = new Logger();
|
||||
this.ipcLoggerFilePath_ = `${profilePath}/log-cross-app-ipc.txt`;
|
||||
this.ipcLogger_.addTarget(TargetType.File, {
|
||||
path: `${profilePath}/log-cross-app-ipc.txt`,
|
||||
path: this.ipcLoggerFilePath_,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +117,14 @@ export default class ElectronAppWrapper {
|
||||
return BrowserWindow.getFocusedWindow() ?? this.win_;
|
||||
}
|
||||
|
||||
public ipcServerStarted() {
|
||||
return !!this.ipcServer_;
|
||||
}
|
||||
|
||||
public ipcLoggerFilePath() {
|
||||
return this.ipcLoggerFilePath_;
|
||||
}
|
||||
|
||||
public windowById(joplinId: string) {
|
||||
if (joplinId === defaultWindowId) {
|
||||
return this.mainWindow();
|
||||
@@ -127,6 +137,24 @@ export default class ElectronAppWrapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
private windowIdFromWebContents(webContents: WebContents): SecondaryWindowId|null {
|
||||
const browserWindow = BrowserWindow.fromWebContents(webContents);
|
||||
// Convert from electron IDs to Joplin IDs.
|
||||
const targetElectronId = browserWindow.id;
|
||||
|
||||
if (this.win_?.id === targetElectronId) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
for (const [joplinId, { electronId }] of this.secondaryWindows_) {
|
||||
if (electronId === targetElectronId) {
|
||||
return joplinId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public allAppWindows() {
|
||||
const allWindowIds = [...this.secondaryWindows_.keys(), defaultWindowId];
|
||||
return allWindowIds.map(id => this.windowById(id));
|
||||
@@ -205,7 +233,10 @@ export default class ElectronAppWrapper {
|
||||
height: windowState.height,
|
||||
minWidth: 100,
|
||||
minHeight: 100,
|
||||
backgroundColor: '#fff', // required to enable sub pixel rendering, can't be in css
|
||||
// A backgroundColor is needed to enable sub-pixel rendering.
|
||||
// Based on https://www.electronjs.org/docs/latest/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do,
|
||||
// this needs to be a non-transparent color:
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#333' : '#fff',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
@@ -312,6 +343,14 @@ export default class ElectronAppWrapper {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const sendWindowFocused = (focusedWebContents: WebContents) => {
|
||||
const joplinId = this.windowIdFromWebContents(focusedWebContents);
|
||||
|
||||
if (joplinId !== null) {
|
||||
this.win_.webContents.send('window-focused', joplinId);
|
||||
}
|
||||
};
|
||||
|
||||
const addWindowEventHandlers = (webContents: WebContents) => {
|
||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||
webContents.on('will-frame-navigate', event => {
|
||||
@@ -344,6 +383,11 @@ export default class ElectronAppWrapper {
|
||||
webContents.on('did-create-window', (event) => {
|
||||
addWindowEventHandlers(event.webContents);
|
||||
});
|
||||
|
||||
const onFocus = () => {
|
||||
sendWindowFocused(webContents);
|
||||
};
|
||||
webContents.on('focus', onFocus);
|
||||
};
|
||||
addWindowEventHandlers(this.win_.webContents);
|
||||
|
||||
@@ -415,6 +459,10 @@ export default class ElectronAppWrapper {
|
||||
this.win_.close();
|
||||
}
|
||||
});
|
||||
|
||||
if (window.isFocused()) {
|
||||
sendWindowFocused(window.webContents);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -582,14 +630,18 @@ export default class ElectronAppWrapper {
|
||||
|
||||
if (port === null) port = this.ipcStartPort_;
|
||||
|
||||
return await sendMessage(port, {
|
||||
...message,
|
||||
sourcePort: this.ipcServer_.port,
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
...options,
|
||||
});
|
||||
if (this.ipcServer_) {
|
||||
return await sendMessage(port, {
|
||||
...message,
|
||||
sourcePort: this.ipcServer_.port,
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async ensureSingleInstance() {
|
||||
@@ -675,32 +727,39 @@ export default class ElectronAppWrapper {
|
||||
|
||||
this.ipcLogger_.info('Starting server using secret key:', secretKeyFilePath);
|
||||
|
||||
this.ipcServer_ = await startServer(this.ipcStartPort_, secretKeyFilePath, async (message) => {
|
||||
if (messageHandlers[message.action]) {
|
||||
this.ipcLogger_.info('Got message:', message);
|
||||
return messageHandlers[message.action](message);
|
||||
}
|
||||
try {
|
||||
this.ipcServer_ = await startServer(this.ipcStartPort_, secretKeyFilePath, async (message) => {
|
||||
if (messageHandlers[message.action]) {
|
||||
this.ipcLogger_.info('Got message:', message);
|
||||
return messageHandlers[message.action](message);
|
||||
}
|
||||
|
||||
throw newHttpError(404);
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
});
|
||||
throw newHttpError(404);
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
});
|
||||
} catch (error) {
|
||||
this.ipcLogger_.error('Could not start server:', error);
|
||||
this.ipcServer_ = null;
|
||||
}
|
||||
|
||||
// First check that no other app is running from that profile folder
|
||||
const gotAppLock = await this.profileLocker_.lock();
|
||||
if (gotAppLock) return false;
|
||||
|
||||
const message: Message = {
|
||||
action: 'onSecondInstance',
|
||||
data: {
|
||||
senderPort: this.ipcServer_.port,
|
||||
profilePath: this.profilePath_,
|
||||
argv: process.argv,
|
||||
},
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
};
|
||||
if (this.ipcServer_) {
|
||||
const message: Message = {
|
||||
action: 'onSecondInstance',
|
||||
data: {
|
||||
senderPort: this.ipcServer_.port,
|
||||
profilePath: this.profilePath_,
|
||||
argv: process.argv,
|
||||
},
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
};
|
||||
|
||||
await this.sendCrossAppIpcMessage(message);
|
||||
await this.sendCrossAppIpcMessage(message);
|
||||
}
|
||||
|
||||
this.quit();
|
||||
if (this.env() === 'dev') console.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
|
||||
|
||||
@@ -111,8 +111,12 @@ export default class InteropServiceHelper {
|
||||
// 2024-01-31: Printing with webContents.print still
|
||||
// fails on Linux (even if run in the main process).
|
||||
// As such, we use window.print(), which seems to work.
|
||||
//
|
||||
// 2025-05-03: Windows and MacOS also need the window.print() workaround.
|
||||
// See https://github.com/electron/electron/pull/46937.
|
||||
|
||||
if (shim.isLinux()) {
|
||||
const applyWorkaround = true;
|
||||
if (applyWorkaround) {
|
||||
await win.webContents.executeJavaScript(`
|
||||
// Blocks while the print dialog is open
|
||||
window.print();
|
||||
|
||||
@@ -4,7 +4,7 @@ import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
describe('app.reducer', () => {
|
||||
|
||||
it('should handle DIALOG_OPEN', async () => {
|
||||
const state: AppState = createAppDefaultState({}, {});
|
||||
const state: AppState = createAppDefaultState({});
|
||||
|
||||
let newState = appReducer(state, {
|
||||
type: 'DIALOG_OPEN',
|
||||
@@ -49,7 +49,7 @@ describe('app.reducer', () => {
|
||||
|
||||
it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => {
|
||||
const state: AppState = {
|
||||
...createAppDefaultState({}, {}),
|
||||
...createAppDefaultState({}),
|
||||
backgroundWindows: {
|
||||
testWindow: {
|
||||
...createAppDefaultWindowState(),
|
||||
|
||||
@@ -54,8 +54,6 @@ export interface AppState extends State, AppWindowState {
|
||||
route: AppStateRoute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navHistory: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
windowContentSize: any;
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
focusedField: string;
|
||||
@@ -81,7 +79,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function createAppDefaultState(windowContentSize: any, resourceEditWatcherDefaultState: any): AppState {
|
||||
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
...createAppDefaultWindowState(),
|
||||
@@ -91,7 +89,6 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche
|
||||
props: {},
|
||||
},
|
||||
navHistory: [],
|
||||
windowContentSize, // bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
@@ -166,12 +163,6 @@ export default function(state: AppState, action: any) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WINDOW_CONTENT_SIZE_SET':
|
||||
|
||||
newState = { ...state };
|
||||
newState.windowContentSize = action.size;
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_TOGGLE':
|
||||
|
||||
{
|
||||
|
||||
@@ -65,10 +65,7 @@ const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
];
|
||||
|
||||
const appDefaultState = createAppDefaultState(
|
||||
bridge().windowContentSize(),
|
||||
resourceEditWatcherDefaultState,
|
||||
);
|
||||
const appDefaultState = createAppDefaultState(resourceEditWatcherDefaultState);
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -149,6 +146,10 @@ class Application extends BaseApplication {
|
||||
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'featureFlag.autoUpdaterServiceEnabled' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) this.setupAutoUpdaterService();
|
||||
}
|
||||
|
||||
const result = await super.generalMiddleware(store, next, action);
|
||||
const newState = store.getState();
|
||||
|
||||
@@ -331,18 +332,6 @@ class Application extends BaseApplication {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
public crashDetectionHandler() {
|
||||
// This handler conflicts with the single instance behaviour, so it's
|
||||
// not used for now.
|
||||
// https://discourse.joplinapp.org/t/pre-release-v2-8-is-now-available-updated-27-april/25158/56?u=laurent
|
||||
if (!Setting.value('wasClosedSuccessfully')) {
|
||||
const answer = confirm(_('The application did not close properly. Would you like to start in safe mode?'));
|
||||
Setting.setValue('isSafeMode', !!answer);
|
||||
}
|
||||
|
||||
Setting.setValue('wasClosedSuccessfully', false);
|
||||
}
|
||||
|
||||
private async setupOcrService() {
|
||||
if (Setting.value('ocr.clearLanguageDataCache')) {
|
||||
Setting.setValue('ocr.clearLanguageDataCache', false);
|
||||
@@ -386,6 +375,8 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
private setupAutoUpdaterService() {
|
||||
this.logger().info('Setting up auto-updater service...');
|
||||
|
||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
bridge().electronApp().initializeAutoUpdaterService(
|
||||
Logger.create('AutoUpdaterService'),
|
||||
@@ -412,6 +403,14 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
|
||||
private async setupIntegrationTestUtils() {
|
||||
// Events used by Playwright tests to quickly change the value of a setting.
|
||||
ipcRenderer.on('testing--setSetting', (_event, key, value) => {
|
||||
this.logger().info('Updating setting using testing API: %s = %s', key, JSON.stringify(value));
|
||||
Setting.setValue(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -420,6 +419,8 @@ class Application extends BaseApplication {
|
||||
|
||||
argv = await super.start(argv, startOptions);
|
||||
|
||||
await this.setupIntegrationTestUtils();
|
||||
|
||||
bridge().setLogFilePath(Logger.globalLogger.logFilePath());
|
||||
|
||||
await this.applySettingsSideEffects();
|
||||
@@ -664,13 +665,15 @@ class Application extends BaseApplication {
|
||||
Setting.setValue('linking.extraAllowedExtensions', newExtensions);
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
ipcRenderer.on('window-focused', (_event, newWindowId) => {
|
||||
const currentWindowId = this.store().getState().windowId;
|
||||
this.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: 'default',
|
||||
lastWindowId: currentWindowId,
|
||||
});
|
||||
if (newWindowId !== currentWindowId) {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: newWindowId,
|
||||
lastWindowId: currentWindowId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await this.initPluginService();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import shim, { MessageBoxType } from '@joplin/lib/shim';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem } from 'electron';
|
||||
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { urlDecode } from '@joplin/lib/string-utils';
|
||||
@@ -246,7 +246,7 @@ export class Bridge {
|
||||
// version of electron-context-menu.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public setupContextMenu(_spellCheckerMenuItemsHandler: Function) {
|
||||
require('electron-context-menu')({
|
||||
require('./services/electron-context-menu')({
|
||||
allWindows: [this.mainWindow()],
|
||||
|
||||
electronApp: this.electronApp(),
|
||||
@@ -313,13 +313,6 @@ export class Bridge {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
// Note: This provides the size of the main window. Prefer CSS where possible.
|
||||
public windowContentSize() {
|
||||
if (!this.mainWindow()) return { width: 0, height: 0 };
|
||||
const s = this.mainWindow().getContentSize();
|
||||
return { width: s[0], height: s[1] };
|
||||
}
|
||||
|
||||
public windowSetSize(width: number, height: number) {
|
||||
if (!this.mainWindow()) return;
|
||||
return this.mainWindow().setSize(width, height);
|
||||
@@ -526,16 +519,30 @@ export class Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
public async launchAltAppInstance(env: string) {
|
||||
const cmd = this.appLaunchCommand(env, 'alt1');
|
||||
private async launchAppInstanceById(env: string, altInstanceId: string) {
|
||||
if (this.electronApp().ipcServerStarted()) {
|
||||
const cmd = this.appLaunchCommand(env, altInstanceId);
|
||||
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
|
||||
} else {
|
||||
const buttonIndex = this.showErrorMessageBox('Cannot launch another instance because IPC server could not start.', {
|
||||
buttons: [
|
||||
_('OK'),
|
||||
_('Open log'),
|
||||
],
|
||||
});
|
||||
|
||||
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
|
||||
if (buttonIndex === 1) {
|
||||
void this.openItem(this.electronApp().ipcLoggerFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async launchAltAppInstance(env: string) {
|
||||
await this.launchAppInstanceById(env, 'alt1');
|
||||
}
|
||||
|
||||
public async launchMainAppInstance(env: string) {
|
||||
const cmd = this.appLaunchCommand(env, '');
|
||||
|
||||
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
|
||||
await this.launchAppInstanceById(env, '');
|
||||
}
|
||||
|
||||
public async restart() {
|
||||
@@ -588,6 +595,11 @@ export class Bridge {
|
||||
return nativeImage.createFromPath(path);
|
||||
}
|
||||
|
||||
public menuPopupFromTemplate(template: ((MenuItemConstructorOptions) | (MenuItem))[]) {
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
return menu.popup({ window: this.mainWindow() });
|
||||
}
|
||||
|
||||
public safeStorage = {
|
||||
isEncryptionAvailable() {
|
||||
return safeStorage.isEncryptionAvailable();
|
||||
|
||||
15
packages/app-desktop/commands/copyToClipboard.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
import { clipboard } from 'electron';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'copyToClipboard',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context, content: string) => {
|
||||
if (!content || (typeof content !== 'string')) return;
|
||||
clipboard.writeText(content);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -38,7 +38,7 @@ describe('exportDeletionLog', () => {
|
||||
let state: AppState = undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
state = createAppDefaultState({}, {});
|
||||
state = createAppDefaultState({});
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-09-18T12:00:00Z').getTime());
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as copyDevCommand from './copyDevCommand';
|
||||
import * as copyToClipboard from './copyToClipboard';
|
||||
import * as editProfileConfig from './editProfileConfig';
|
||||
import * as emptyTrash from './emptyTrash';
|
||||
import * as exportDeletionLog from './exportDeletionLog';
|
||||
@@ -24,6 +25,7 @@ import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
||||
|
||||
const index: any[] = [
|
||||
copyDevCommand,
|
||||
copyToClipboard,
|
||||
editProfileConfig,
|
||||
emptyTrash,
|
||||
exportDeletionLog,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'startExternalEditing',
|
||||
|
||||
@@ -35,8 +35,8 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
void shim.showMessageBox(_('Token has been copied to the clipboard!'), { type: MessageBoxType.Info });
|
||||
}
|
||||
|
||||
private renewToken_click() {
|
||||
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
|
||||
private async renewToken_click() {
|
||||
if (await shim.showConfirmationDialog(_('Are you sure you want to renew the authorisation token?'))) {
|
||||
void EncryptionService.instance()
|
||||
.generateApiToken()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
|
||||
@@ -97,17 +97,17 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
private async handleSettingButton(key: string) {
|
||||
if (key === 'sync.clearLocalSyncStateButton') {
|
||||
if (!confirm('This cannot be undone. Do you want to continue?')) return;
|
||||
if (!await shim.showConfirmationDialog('This cannot be undone. Do you want to continue?')) return;
|
||||
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalSyncState);
|
||||
await Setting.saveAll();
|
||||
await restart();
|
||||
} else if (key === 'sync.clearLocalDataButton') {
|
||||
if (!confirm('This cannot be undone. Do you want to continue?')) return;
|
||||
if (!await shim.showConfirmationDialog('This cannot be undone. Do you want to continue?')) return;
|
||||
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData);
|
||||
await Setting.saveAll();
|
||||
await restart();
|
||||
} else if (key === 'ocr.clearLanguageDataCacheButton') {
|
||||
if (!confirm(this.restartMessage())) return;
|
||||
if (!await shim.showConfirmationDialog(this.restartMessage())) return;
|
||||
Setting.setValue('ocr.clearLanguageDataCache', true);
|
||||
await restart();
|
||||
} else if (key === 'sync.openSyncWizard') {
|
||||
@@ -258,6 +258,28 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
);
|
||||
}
|
||||
|
||||
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServerSaml')) {
|
||||
const server = settings['sync.11.path'] as string;
|
||||
|
||||
const goToSamlLogin = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'JoplinServerSamlLogin',
|
||||
});
|
||||
};
|
||||
|
||||
settingComps.push(
|
||||
<div key="connect_to_joplin_server_saml_button" style={this.rowStyle_}>
|
||||
<Button
|
||||
title={_('Connect using your organisation account')}
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={goToSamlLogin}
|
||||
disabled={!server || server?.trim().length === 0}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
settingComps.push(
|
||||
<div key="check_sync_config_button" style={this.rowStyle_}>
|
||||
<Button
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
@@ -3,9 +3,14 @@ import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import PluginService, { Plugins } from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import restart from '../services/restart';
|
||||
import BannerContent from './NoteEditor/WarningBanner/BannerContent';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
const logger = Logger.create('ErrorBoundary');
|
||||
|
||||
interface ErrorInfo {
|
||||
componentStack: string;
|
||||
}
|
||||
@@ -30,6 +35,28 @@ interface Props {
|
||||
children: any;
|
||||
}
|
||||
|
||||
interface BannerProps {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const SwitchToNewEditorBanner = (props: BannerProps) => {
|
||||
|
||||
const handleSwitchToNewEditor = () => {
|
||||
Setting.setValue('editor.legacyMarkdown', false);
|
||||
const message = _('You are now using the latest version of the Markdown editor.');
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
alert(message);
|
||||
};
|
||||
|
||||
return <BannerContent
|
||||
acceptMessage={_('Switch to the new editor')}
|
||||
onAccept={handleSwitchToNewEditor}
|
||||
visible={props.isVisible}
|
||||
>
|
||||
{_('The legacy Markdown editor appears to have crashed due to an incompatibility with a plugin. We recommend using the new editor.')}
|
||||
</BannerContent>;
|
||||
};
|
||||
|
||||
export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
|
||||
public state: State = { error: null, errorInfo: null, pluginInfos: [], plugins: {} };
|
||||
@@ -59,6 +86,9 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
this.setState({ error, errorInfo, pluginInfos, plugins });
|
||||
|
||||
logger.error('The application encountered an error:', error);
|
||||
logger.error('Component stack', errorInfo?.componentStack);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
@@ -130,8 +160,11 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const isLegacyEditorError = !!this.state.error.stack.includes('CodeMirror/v5');
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'auto', fontFamily: 'sans-serif', padding: '5px 20px' }}>
|
||||
<SwitchToNewEditorBanner isVisible={isLegacyEditorError} />
|
||||
<h1>Error</h1>
|
||||
{this.renderMessage()}
|
||||
<p>To report the error, please copy the *entire content* of this page and post it on Joplin forum or GitHub.</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import EmojiBox from './EmojiBox';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const { connect } = require('react-redux');
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { clipboard } from 'electron';
|
||||
import Button from './Button/Button';
|
||||
import { Fragment } from 'react';
|
||||
import { accountTypeToString } from '@joplin/lib/utils/joplinCloud/types';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
@@ -47,10 +47,10 @@ const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
|
||||
<h2>{_('Email to note')}</h2>
|
||||
<p>{_('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook')}</p>
|
||||
{
|
||||
isEmailToNoteAvailableInAccount ? <Fragment>
|
||||
isEmailToNoteAvailableInAccount ? <>
|
||||
<p className='inbox-email-value'>{props.inboxEmail}</p>
|
||||
<Button onClick={copyToClipboard} title={_('Copy to clipboard')} />
|
||||
</Fragment>
|
||||
</>
|
||||
: <div className='alert-warn'>
|
||||
<p>{_('Your account doesn\'t have access to this feature')}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Fragment, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { clipboard } from 'electron';
|
||||
import Button, { ButtonLevel } from './Button/Button';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import { Dispatch } from 'redux';
|
||||
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
|
||||
@@ -11,6 +11,7 @@ import { AppState } from '../app.reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import JoplinCloudSignUpCallToAction from './JoplinCloudSignUpCallToAction';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -61,7 +62,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
|
||||
const onAuthorizeClicked = async () => {
|
||||
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
|
||||
bridge().openExternal(url);
|
||||
void bridge().openExternal(url);
|
||||
onButtonUsed();
|
||||
};
|
||||
|
||||
@@ -81,7 +82,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
<div className="login-page">
|
||||
<div className="page-container">
|
||||
{state.active !== 'COMPLETED' ? (
|
||||
<Fragment>
|
||||
<>
|
||||
<p className="text">{_('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:')}</p>
|
||||
<div className="buttons-container">
|
||||
<Button
|
||||
@@ -98,7 +99,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
/>
|
||||
|
||||
</div>
|
||||
</Fragment>
|
||||
</>
|
||||
) : null}
|
||||
<p className={state.className}>{state.message()}
|
||||
{state.active === 'ERROR' ? (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { AppState, AppStateRoute } from '../app.reducer';
|
||||
import bridge from '../services/bridge';
|
||||
import { useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of code from before rule was applied
|
||||
@@ -55,26 +55,44 @@ const useWindowRefocusManager = (route: AppStateRoute) => {
|
||||
}, [routeName, windowId]);
|
||||
};
|
||||
|
||||
const useContainerSize = (container: HTMLElement|null) => {
|
||||
const [size, setSize] = useState({ width: container?.clientWidth ?? 0, height: container?.clientHeight ?? 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!container) return () => {};
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
setSize({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight,
|
||||
});
|
||||
});
|
||||
observer.observe(container);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [container]);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
const NavigatorComponent: React.FC<Props> = props => {
|
||||
const route = props.route;
|
||||
const screenInfo = props.screens[route?.routeName];
|
||||
const [container, setContainer] = useState<HTMLElement|null>(null);
|
||||
|
||||
useWindowTitleManager(screenInfo);
|
||||
useWindowRefocusManager(route);
|
||||
const size = useContainerSize(container);
|
||||
|
||||
if (!route) throw new Error('Route must not be null');
|
||||
|
||||
const screenProps = route.props ? route.props : {};
|
||||
const Screen = screenInfo.screen;
|
||||
|
||||
const screenStyle = {
|
||||
width: props.style.width,
|
||||
height: props.style.height,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={props.style} className={props.className}>
|
||||
<Screen style={screenStyle} {...screenProps} />
|
||||
<div ref={setContainer} style={props.style} className={props.className}>
|
||||
<Screen style={size} {...screenProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ import { SecondaryWindowApi } from '../utils/window/types';
|
||||
export const WindowIdContext = createContext(defaultWindowId);
|
||||
|
||||
type OnCloseCallback = ()=> void;
|
||||
type OnFocusCallback = ()=> void;
|
||||
|
||||
export enum WindowMode {
|
||||
Iframe, NewWindow,
|
||||
@@ -27,7 +26,6 @@ interface Props {
|
||||
mode: WindowMode;
|
||||
windowId: string;
|
||||
onClose: OnCloseCallback;
|
||||
onFocus?: OnFocusCallback;
|
||||
}
|
||||
|
||||
const useDocument = (
|
||||
@@ -86,10 +84,7 @@ const useDocument = (
|
||||
};
|
||||
|
||||
type OnSetLoaded = (loaded: boolean)=> void;
|
||||
const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: OnFocusCallback) => {
|
||||
const onFocusRef = useRef(onFocus);
|
||||
onFocusRef.current = onFocus;
|
||||
|
||||
const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded) => {
|
||||
useEffect(() => {
|
||||
if (!doc) return;
|
||||
|
||||
@@ -120,14 +115,6 @@ const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?:
|
||||
|
||||
doc.body.style.height = '100vh';
|
||||
|
||||
const containerWindow = doc.defaultView;
|
||||
containerWindow.addEventListener('focus', () => {
|
||||
onFocusRef.current?.();
|
||||
});
|
||||
if (doc.hasFocus()) {
|
||||
onFocusRef.current?.();
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
}, [doc, setLoaded]);
|
||||
};
|
||||
@@ -137,7 +124,7 @@ const NewWindowOrIFrame: React.FC<Props> = props => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const doc = useDocument(props.mode, iframeRef, props.onClose);
|
||||
useDocumentSetup(doc, setLoaded, props.onFocus);
|
||||
useDocumentSetup(doc, setLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doc) return;
|
||||
|
||||
@@ -20,7 +20,6 @@ interface Props {
|
||||
|
||||
newWindow: boolean;
|
||||
windowId: string;
|
||||
activeWindowId: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
}
|
||||
|
||||
@@ -57,22 +56,10 @@ const SecondaryWindow: React.FC<Props> = props => {
|
||||
}
|
||||
}, [props.dispatch, props.windowId, newWindow]);
|
||||
|
||||
const onWindowFocus = useCallback(() => {
|
||||
// Verify that the window still has focus (e.g. to handle the case where the event was delayed).
|
||||
if (containerRef.current?.ownerDocument.hasFocus()) {
|
||||
props.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: props.windowId,
|
||||
lastWindowId: props.activeWindowId,
|
||||
});
|
||||
}
|
||||
}, [props.dispatch, props.windowId, props.activeWindowId]);
|
||||
|
||||
return <NewWindowOrIFrame
|
||||
mode={newWindow ? WindowMode.NewWindow : WindowMode.Iframe}
|
||||
windowId={props.windowId}
|
||||
onClose={onWindowClose}
|
||||
onFocus={onWindowFocus}
|
||||
title={windowTitle}
|
||||
>
|
||||
<LibraryStyleRoot>
|
||||
@@ -122,7 +109,6 @@ export default connect((state: AppState, ownProps: ConnectProps) => {
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'],
|
||||
legacyMarkdown: state.settings['editor.legacyMarkdown'],
|
||||
activeWindowId: stateUtils.activeWindowId(state),
|
||||
startupPluginsLoaded: state.startupPluginsLoaded,
|
||||
};
|
||||
})(SecondaryWindow);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, import/prefer-default-export -- Old code before rule was applied
|
||||
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
if (!body) return 0;
|
||||
|
||||
@@ -20,12 +18,3 @@ export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function usePrevious(value: any): any {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
|
||||
import * as CodeMirror from 'codemirror';
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import 'codemirror/addon/comment/comment';
|
||||
import 'codemirror/addon/dialog/dialog';
|
||||
@@ -32,54 +32,41 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
// import eventManager from '@joplin/lib/eventManager';
|
||||
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
// Based on http://pypl.github.io/PYPL.html
|
||||
const topLanguages = [
|
||||
'python',
|
||||
'clike',
|
||||
'javascript',
|
||||
'jsx',
|
||||
'php',
|
||||
'r',
|
||||
'swift',
|
||||
'go',
|
||||
'vb',
|
||||
'vbscript',
|
||||
'ruby',
|
||||
'rust',
|
||||
'dart',
|
||||
'lua',
|
||||
'groovy',
|
||||
'perl',
|
||||
'cobol',
|
||||
'julia',
|
||||
'haskell',
|
||||
'pascal',
|
||||
'css',
|
||||
import 'codemirror/mode/python/python';
|
||||
import 'codemirror/mode/clike/clike';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/jsx/jsx';
|
||||
import 'codemirror/mode/php/php';
|
||||
import 'codemirror/mode/r/r';
|
||||
import 'codemirror/mode/swift/swift';
|
||||
import 'codemirror/mode/go/go';
|
||||
import 'codemirror/mode/vb/vb';
|
||||
import 'codemirror/mode/vbscript/vbscript';
|
||||
import 'codemirror/mode/ruby/ruby';
|
||||
import 'codemirror/mode/rust/rust';
|
||||
import 'codemirror/mode/dart/dart';
|
||||
import 'codemirror/mode/lua/lua';
|
||||
import 'codemirror/mode/groovy/groovy';
|
||||
import 'codemirror/mode/perl/perl';
|
||||
import 'codemirror/mode/cobol/cobol';
|
||||
import 'codemirror/mode/julia/julia';
|
||||
import 'codemirror/mode/haskell/haskell';
|
||||
import 'codemirror/mode/pascal/pascal';
|
||||
import 'codemirror/mode/css/css';
|
||||
|
||||
// Additional languages, not in the PYPL list
|
||||
'xml', // For HTML too
|
||||
'markdown',
|
||||
'yaml',
|
||||
'shell',
|
||||
'dockerfile',
|
||||
'diff',
|
||||
'erlang',
|
||||
'sql',
|
||||
];
|
||||
// Load Top Modes
|
||||
for (let i = 0; i < topLanguages.length; i++) {
|
||||
const mode = topLanguages[i];
|
||||
// Additional languages, not in the PYPL lis;
|
||||
import 'codemirror/mode/xml/xml'; // For HTML too
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
import 'codemirror/mode/shell/shell';
|
||||
import 'codemirror/mode/dockerfile/dockerfile';
|
||||
import 'codemirror/mode/diff/diff';
|
||||
import 'codemirror/mode/erlang/erlang';
|
||||
import 'codemirror/mode/sql/sql';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
if (CodeMirror.modeInfo.find((m: any) => m.mode === mode)) {
|
||||
require(`codemirror/mode/${mode}/${mode}`);
|
||||
} else {
|
||||
reg.logger().error('Cannot find CodeMirror mode: ', mode);
|
||||
}
|
||||
}
|
||||
|
||||
export interface EditorProps {
|
||||
value: string;
|
||||
|
||||
@@ -26,7 +26,7 @@ interface Props extends EditorProps {
|
||||
}
|
||||
|
||||
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
const editorContainerRef = useRef<HTMLDivElement>();
|
||||
const editorContainerRef = useRef<HTMLDivElement|null>(null);
|
||||
const [editor, setEditor] = useState<CodeMirrorControl|null>(null);
|
||||
|
||||
// The editor will only be created once, so callbacks that could
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from
|
||||
import { NoteBodyEditorProps, NoteBodyEditorRef } from '../../utils/types';
|
||||
|
||||
const PlainEditor = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
|
||||
const editorRef = useRef<HTMLTextAreaElement>();
|
||||
const editorRef = useRef<HTMLTextAreaElement|null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler, ScrollToTextValue } from '../../utils/types';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo, Ref } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler, ScrollToTextValue, NoteBodyEditorRef } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml } from '../../utils/resourceHandling';
|
||||
import attachedResources from '@joplin/lib/utils/attachedResources';
|
||||
import useScroll from './utils/useScroll';
|
||||
@@ -44,7 +44,7 @@ import useDocument from '../../../hooks/useDocument';
|
||||
import useEditDialog from './utils/useEditDialog';
|
||||
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useTextPatternsLookup from './utils/useTextPatternsLookup';
|
||||
import useTextPatternsLookup, { TextPatternContext } from './utils/useTextPatternsLookup';
|
||||
import { toFileProtocolPath } from '@joplin/utils/path';
|
||||
import { RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
|
||||
@@ -95,8 +95,7 @@ interface LastOnChangeEventInfo {
|
||||
let dispatchDidUpdateIID_: any = null;
|
||||
let changeId_ = 1;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
const [editorContainer, setEditorContainer] = useState<HTMLDivElement|null>(null);
|
||||
const editorContainerDom = useDocument(editorContainer);
|
||||
const [editor, setEditor] = useState<Editor|null>(null);
|
||||
@@ -765,7 +764,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
forecolor: { inline: 'span', styles: { color: '%value' }, remove_similar: true },
|
||||
},
|
||||
text_patterns: [],
|
||||
text_patterns_lookup: () => textPatternsLookupRef.current(),
|
||||
text_patterns_lookup: (ctx: TextPatternContext) => textPatternsLookupRef.current(ctx),
|
||||
|
||||
setup: (editor: Editor) => {
|
||||
editor.addCommand('joplinMath', async () => {
|
||||
@@ -1129,7 +1128,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const allAssetsOptions: NoteStyleOptions = {
|
||||
contentMaxWidthTarget: '.mce-content-body',
|
||||
contentWrapperSelector: '.mce-content-body',
|
||||
scrollbarSize: props.scrollbarSize,
|
||||
themeId: props.contentMarkupLanguage === MarkupLanguage.Html ? 1 : null,
|
||||
whiteBackgroundNoteRendering: props.whiteBackgroundNoteRendering,
|
||||
};
|
||||
@@ -1146,7 +1144,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editor, props.noteId, props.themeId, props.scrollbarSize, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
|
||||
}, [editor, props.noteId, props.themeId, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
@@ -1200,11 +1198,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// we call the current one from setTimeout.
|
||||
// https://github.com/facebook/react/issues/14010#issuecomment-433788147
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const props_onChangeRef = useRef<Function>();
|
||||
const props_onChangeRef = useRef<Function>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const prop_htmlToMarkdownRef = useRef<HtmlToMarkdownHandler>();
|
||||
const prop_htmlToMarkdownRef = useRef<HtmlToMarkdownHandler>(null);
|
||||
prop_htmlToMarkdownRef.current = props.htmlToMarkdown;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -17,6 +17,7 @@ import { EditDialogControl } from './useEditDialog';
|
||||
import { Dispatch } from 'redux';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import type { MenuItem as MenuItemType } from 'electron';
|
||||
import isItemId from '@joplin/lib/models/utils/isItemId';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -38,15 +39,20 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
const makeMainMenuItems = (element: Element) => {
|
||||
let itemType: ContextMenuItemType = ContextMenuItemType.None;
|
||||
let resourceId = '';
|
||||
let linkToCopy = null;
|
||||
let linkUrl = null;
|
||||
|
||||
const pathToId = (path: string) => {
|
||||
const id = Resource.pathToId(path);
|
||||
return isItemId(id) ? id : '';
|
||||
};
|
||||
|
||||
if (element.nodeName === 'IMG') {
|
||||
itemType = ContextMenuItemType.Image;
|
||||
resourceId = Resource.pathToId((element as HTMLImageElement).src);
|
||||
resourceId = pathToId((element as HTMLImageElement).src);
|
||||
} else if (element.nodeName === 'A') {
|
||||
resourceId = Resource.pathToId((element as HTMLAnchorElement).href);
|
||||
resourceId = pathToId((element as HTMLAnchorElement).href);
|
||||
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
|
||||
linkToCopy = element.getAttribute('href') || '';
|
||||
linkUrl = element.getAttribute('href') || '';
|
||||
} else {
|
||||
itemType = ContextMenuItemType.Text;
|
||||
}
|
||||
@@ -56,7 +62,8 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
resourceId,
|
||||
filename: null,
|
||||
mime: null,
|
||||
linkToCopy,
|
||||
linkToCopy: linkUrl,
|
||||
linkToOpen: linkUrl,
|
||||
textToCopy: null,
|
||||
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
||||
insertContent: (content: string) => {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export interface TextPatternContext {
|
||||
readonly text: string;
|
||||
readonly block: Element;
|
||||
}
|
||||
|
||||
interface TextPatternOptions {
|
||||
enabled: boolean;
|
||||
enableMath: boolean;
|
||||
}
|
||||
|
||||
const useTextPatternsLookup = ({ enabled, enableMath }: TextPatternOptions) => {
|
||||
const getTextPatterns = () => {
|
||||
const getTextPatterns = (ctx: TextPatternContext) => {
|
||||
if (!enabled) return [];
|
||||
|
||||
return [
|
||||
@@ -27,6 +32,8 @@ const useTextPatternsLookup = ({ enabled, enableMath }: TextPatternOptions) => {
|
||||
{ start: '1.', cmd: 'InsertOrderedList' },
|
||||
{ start: '*', cmd: 'InsertUnorderedList' },
|
||||
{ start: '-', cmd: 'InsertUnorderedList' },
|
||||
// To more closely match Markdown, only do <hr/> replacements if the full line matches the pattern.
|
||||
['---', '***', '___'].includes(ctx.text) && { start: ctx.text, replacement: '<hr/>' },
|
||||
].filter(pattern => !!pattern);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote';
|
||||
import useEffectiveNoteId from './utils/useEffectiveNoteId';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
||||
import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEditorRef, NoteBodyEditorPropsAndRef } from './utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
@@ -52,10 +52,10 @@ import Logger from '@joplin/utils/Logger';
|
||||
import usePluginEditorView from './utils/usePluginEditorView';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@@ -75,23 +75,17 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
|
||||
|
||||
const editorRef = useRef<NoteBodyEditorRef>();
|
||||
const titleInputRef = useRef<HTMLInputElement>();
|
||||
const editorRef = useRef<NoteBodyEditorRef|null>(null);
|
||||
const titleInputRef = useRef<HTMLInputElement|null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
|
||||
const editorPluginHandler = useMemo(() => {
|
||||
return new EditorPluginHandler(PluginService.instance());
|
||||
}, []);
|
||||
|
||||
const shownEditorViewIds = props['plugins.shownEditorViewIds'];
|
||||
|
||||
// Should be constant and unique to this instance of the editor.
|
||||
const editorId = useMemo(() => {
|
||||
return `editor-${editorIdCounter++}`;
|
||||
}, []);
|
||||
|
||||
const setFormNoteRef = useRef<OnSetFormNote>();
|
||||
const setFormNoteRef = useRef<OnSetFormNote>(null);
|
||||
const { saveNoteIfWillChange, scheduleSaveNote } = useScheduleSaveCallbacks({
|
||||
setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef, editorId,
|
||||
});
|
||||
@@ -105,18 +99,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
}, []);
|
||||
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
|
||||
useAsyncEffect(async (_event) => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
await editorPluginHandler.emitActivationCheck();
|
||||
}, [effectiveNoteId, editorPluginHandler, props.startupPluginsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
editorPluginHandler.emitUpdate(shownEditorViewIds);
|
||||
}, [effectiveNoteId, editorPluginHandler, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds);
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins);
|
||||
const builtInEditorVisible = !editorPlugin;
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
@@ -130,11 +113,24 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
editorId,
|
||||
});
|
||||
setFormNoteRef.current = setFormNote;
|
||||
const formNoteRef = useRef<FormNote>();
|
||||
const formNoteRef = useRef<FormNote>(formNote);
|
||||
formNoteRef.current = { ...formNote };
|
||||
|
||||
const formNoteFolder = useFolder({ folderId: formNote.parent_id });
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const shownEditorViewIds = useVisiblePluginEditorViewIds(props.plugins, windowId);
|
||||
useConnectToEditorPlugin({
|
||||
startupPluginsLoaded: props.startupPluginsLoaded,
|
||||
setFormNote,
|
||||
scheduleSaveNote,
|
||||
formNote,
|
||||
effectiveNoteId,
|
||||
shownEditorViewIds,
|
||||
activeEditorView: editorView,
|
||||
plugins: props.plugins,
|
||||
});
|
||||
|
||||
const {
|
||||
localSearch,
|
||||
onChange: localSearch_change,
|
||||
@@ -161,6 +157,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
customCss: props.customCss,
|
||||
plugins: props.plugins,
|
||||
scrollbarSize: props.scrollbarSize,
|
||||
baseFontFamily: props.viewerFontFamily,
|
||||
});
|
||||
|
||||
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null) => {
|
||||
@@ -180,9 +177,10 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
contentMaxWidthTarget: options.contentMaxWidthTarget,
|
||||
scrollbarSize: props.scrollbarSize,
|
||||
baseFontFamily: props.viewerFontFamily,
|
||||
whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering,
|
||||
});
|
||||
}, [props.plugins, props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
|
||||
}, [props.plugins, props.themeId, props.scrollbarSize, props.viewerFontFamily, props.customCss, props.contentMaxWidth]);
|
||||
|
||||
const handleProvisionalFlag = useCallback(() => {
|
||||
if (props.isProvisional) {
|
||||
@@ -336,7 +334,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
});
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
useResourceUnwatcher({ noteId: formNote.id, windowId });
|
||||
@@ -423,7 +420,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
|
||||
const markupLanguage = formNote.markup_language;
|
||||
const editorProps: NoteBodyEditorProps = {
|
||||
const editorProps: NoteBodyEditorPropsAndRef = {
|
||||
ref: editorRef,
|
||||
contentKey: formNote.id,
|
||||
style: styles.tinyMCE,
|
||||
@@ -462,6 +459,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
fontSize: Setting.value('style.editor.fontSize'),
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
scrollbarSize: props.scrollbarSize,
|
||||
baseFontFamily: props.viewerFontFamily,
|
||||
isSafeMode: props.isSafeMode,
|
||||
useCustomPdfViewer: props.useCustomPdfViewer,
|
||||
// We need it to identify the context for which media is rendered.
|
||||
@@ -706,7 +704,6 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'historyBackward',
|
||||
'historyForward',
|
||||
@@ -718,6 +715,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
], whenClauseContext)[0] as ToolbarButtonInfo,
|
||||
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
|
||||
scrollbarSize: state.settings['style.scrollbarSize'],
|
||||
viewerFontFamily: state.settings['style.viewer.fontFamily'],
|
||||
tabMovesFocus: state.settings['editor.tabMovesFocus'],
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
useCustomPdfViewer: false,
|
||||
|
||||
@@ -5,7 +5,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
acceptMessage: string;
|
||||
onAccept: ()=> void;
|
||||
onDismiss: ()=> void;
|
||||
onDismiss?: ()=> void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const BannerContent: React.FC<Props> = props => {
|
||||
return <div className='warning-banner'>
|
||||
{props.children}
|
||||
<a onClick={props.onAccept} className='warning-banner-link' href="#">[ {props.acceptMessage} ]</a>
|
||||
<a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {_('Dismiss')} ]</a>
|
||||
{ props.onDismiss ? <a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {_('Dismiss')} ]</a> : null }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { openFileWithExternalEditor } from '@joplin/lib/services/ExternalEditWatcher/utils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
const fs = require('fs-extra');
|
||||
const { writeFile } = require('fs-extra');
|
||||
const { clipboard } = require('electron');
|
||||
@@ -84,9 +85,18 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
open: {
|
||||
label: _('Open...'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
await openItemById(options.resourceId, dispatch);
|
||||
if (options.resourceId) {
|
||||
await openItemById(options.resourceId, dispatch);
|
||||
} else if (options.linkToOpen) {
|
||||
await CommandService.instance().execute('openItem', options.linkToOpen);
|
||||
} else {
|
||||
await shim.showErrorDialog('No link found');
|
||||
}
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => (
|
||||
(!options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource))
|
||||
|| (!!options.linkToOpen && itemType === ContextMenuItemType.Link)
|
||||
),
|
||||
},
|
||||
saveAs: {
|
||||
label: _('Save as...'),
|
||||
|
||||