Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
636fbdf7d0 | ||
|
|
ee97434bb0 | ||
|
|
599cf5b86f | ||
|
|
2fd6a3a2fa | ||
|
|
a3e04103de | ||
|
|
320d0df60d | ||
|
|
f32fe63205 | ||
|
|
3e0fb48e44 | ||
|
|
4a475f1b53 | ||
|
|
8679cc5704 | ||
|
|
a48c4ba93f | ||
|
|
16e82b5462 | ||
|
|
1a82255865 | ||
|
|
a64d6e3270 | ||
|
|
ae1620dd50 | ||
|
|
d804e95d06 | ||
|
|
78d9a7e636 | ||
|
|
3270122419 | ||
|
|
e4b8976aa0 | ||
|
|
17e1eecb11 | ||
|
|
dd9a389711 | ||
|
|
55eaedb8b2 | ||
|
|
f8d772de87 | ||
|
|
77c39ac084 | ||
|
|
3557138c84 | ||
|
|
77e74112ad | ||
|
|
4115e2054f | ||
|
|
8485277dcf | ||
|
|
fac9ea3b42 | ||
|
|
45f8e27d6a | ||
|
|
c8a478d970 | ||
|
|
75dfb0af5f | ||
|
|
5e592a3096 | ||
|
|
84e46ad874 | ||
|
|
0ec917bb96 | ||
|
|
818f9f58d1 | ||
|
|
e1abe0b4cb | ||
|
|
c972ce223e | ||
|
|
d9dadf28cb | ||
|
|
1fb392ff4e | ||
|
|
affa620983 | ||
|
|
ed31d8202b | ||
|
|
573ea6051c | ||
|
|
f1ec54532f | ||
|
|
5eb96d71e1 | ||
|
|
73251bac4a | ||
|
|
f40a0da195 | ||
|
|
8dc1ab2cc5 | ||
|
|
1b46c9f5e7 | ||
|
|
483ab55a36 | ||
|
|
502002f9f6 | ||
|
|
8d8014511f | ||
|
|
28569e652e | ||
|
|
9acf36d802 | ||
|
|
06d26767ed | ||
|
|
88858d4413 | ||
|
|
27309427a1 | ||
|
|
e83a18a907 | ||
|
|
9bd8b11f67 | ||
|
|
3a14c7ce2d | ||
|
|
fe4c9a2401 | ||
|
|
d2fb19cf6d | ||
|
|
1f8e3fb620 | ||
|
|
8bbe1d30b4 | ||
|
|
ec92f716de | ||
|
|
ab819d9210 | ||
|
|
e465b45d6e | ||
|
|
a4a4170d49 | ||
|
|
1dcf528443 | ||
|
|
8cf4ef88b5 | ||
|
|
bf634270be | ||
|
|
56437d3e1b | ||
|
|
d095ab2be7 | ||
|
|
4751b4dd74 | ||
|
|
ce22d8238c | ||
|
|
9e2b9e5b8d | ||
|
|
4952980e0a | ||
|
|
59989d2735 | ||
|
|
eb7f2855b0 | ||
|
|
a0e3e4fefb | ||
|
|
de9661448b | ||
|
|
562aabafa1 | ||
|
|
a659e45a68 | ||
|
|
8e44a15c8d | ||
|
|
a086358824 | ||
|
|
889c395818 | ||
|
|
7d0cc675aa | ||
|
|
73d3f92ae2 | ||
|
|
f5ceb4064c | ||
|
|
53d7bc86ca | ||
|
|
4495fc9a03 | ||
|
|
7ee5cad21e | ||
|
|
940739ce12 | ||
|
|
5730c1efcd | ||
|
|
b17f28ce94 | ||
|
|
629e968878 | ||
|
|
47a924ff4e | ||
|
|
c511fb59c7 | ||
|
|
80aeff6ecd | ||
|
|
97d15bb26a | ||
|
|
99b36cbff1 | ||
|
|
74c3d2c9fb | ||
|
|
df8c7fd31c | ||
|
|
3a780b9490 | ||
|
|
97ddb67f68 | ||
|
|
f50a27985b | ||
|
|
061a9d5bff | ||
|
|
06f42e8246 | ||
|
|
19f0b667b1 | ||
|
|
ac7165461a | ||
|
|
efb48e6145 | ||
|
|
f94c16b22e | ||
|
|
0938dc9d52 | ||
|
|
e049698012 | ||
|
|
c9fb06fd0c | ||
|
|
96850b7b98 | ||
|
|
32710e44c3 | ||
|
|
32e16f6e51 | ||
|
|
f938d5f489 | ||
|
|
99b840da34 | ||
|
|
55c222c577 | ||
|
|
418a6e455f | ||
|
|
789d19b18c | ||
|
|
59b26f2c63 | ||
|
|
f1eeeabdc5 | ||
|
|
d2a33b006b | ||
|
|
2386f583e8 | ||
|
|
70c5448402 | ||
|
|
4e3326b12f | ||
|
|
34092d8491 | ||
|
|
b8caf08fac | ||
|
|
b0d0e641ea | ||
|
|
3a1de4e941 | ||
|
|
97274c95a5 | ||
|
|
300d0e3ca5 | ||
|
|
50d08cd178 | ||
|
|
28f3d53b3b | ||
|
|
768e59938c | ||
|
|
6c7948a087 | ||
|
|
cc2b442519 | ||
|
|
fa285a9404 | ||
|
|
84faa7229d | ||
|
|
254747ee78 | ||
|
|
ef167051d6 | ||
|
|
1ccbdc2341 | ||
|
|
14747b79cd | ||
|
|
111385f1ef | ||
|
|
f36d395a84 | ||
|
|
5d997084f7 | ||
|
|
132548181f | ||
|
|
452c71e8cb | ||
|
|
f7ac95f850 | ||
|
|
33286efe9a | ||
|
|
f21a93febe | ||
|
|
f817c47dc0 | ||
|
|
9ccca16df7 | ||
|
|
114f5695b7 | ||
|
|
916fa39012 | ||
|
|
db77a51129 | ||
|
|
b09d6e8568 | ||
|
|
3bf9438a59 | ||
|
|
554894e910 | ||
|
|
51f1e0202f | ||
|
|
8b7758442b | ||
|
|
63bf7694f0 | ||
|
|
df3aaa7dfd | ||
|
|
826006ce8b | ||
|
|
1e085ee619 | ||
|
|
efb753e229 | ||
|
|
faf332a0e8 | ||
|
|
83308337b5 | ||
|
|
73193df120 | ||
|
|
2dd27cdd00 | ||
|
|
c5e3672e9e | ||
|
|
366517999f | ||
|
|
8445ffaa86 | ||
|
|
2a76970461 | ||
|
|
9fcaf5bd18 | ||
|
|
652add9af2 | ||
|
|
c632ea5c48 | ||
|
|
7ee5f68770 | ||
|
|
1040675781 | ||
|
|
24a37e0fef | ||
|
|
bfe2d262a5 | ||
|
|
8faf5148a6 | ||
|
|
27c5dd1852 | ||
|
|
fae51b90a7 | ||
|
|
ccd181851c | ||
|
|
cd0ff94c0c | ||
|
|
e9e6d8a69c | ||
|
|
7ad3b34ec3 | ||
|
|
f39021d373 | ||
|
|
13116fec76 | ||
|
|
d49b2ec0e9 | ||
|
|
09d088b2b5 | ||
|
|
3312bd27c9 | ||
|
|
453bdb293f | ||
|
|
6b0e1598ed | ||
|
|
56f25d3094 | ||
|
|
3e458c0028 | ||
|
|
a747828276 | ||
|
|
a90e3e04a4 | ||
|
|
487f01d2ec | ||
|
|
36c25fdd86 | ||
|
|
a9fecb31c3 | ||
|
|
ca8fd8d7ae | ||
|
|
fb345b1317 | ||
|
|
966fe38ae3 | ||
|
|
3042e615ac | ||
|
|
fd2ae51b93 | ||
|
|
1227730393 | ||
|
|
8622bd506f | ||
|
|
5d6a39ce51 | ||
|
|
5245c06ed3 | ||
|
|
3c5977346e | ||
|
|
a3dc9c2721 | ||
|
|
90ec1f5bc6 | ||
|
|
99caa014ca | ||
|
|
00d0cd1cf7 | ||
|
|
d6480e50d2 | ||
|
|
a1a06dd7d0 |
No files matched your search
@@ -291,6 +291,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
@@ -322,6 +323,8 @@ packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
@@ -453,6 +456,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
@@ -461,6 +466,7 @@ packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
@@ -501,20 +507,23 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
@@ -582,11 +591,14 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
@@ -599,26 +611,38 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
@@ -659,7 +683,9 @@ packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
@@ -690,6 +716,7 @@ packages/app-mobile/utils/fs-driver/tarExtract.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -742,8 +769,9 @@ packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
@@ -756,8 +784,22 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/util/setupVim.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
@@ -783,6 +825,7 @@ packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
packages/lib/BaseApplication.js
|
||||
packages/lib/BaseModel.js
|
||||
@@ -872,6 +915,7 @@ packages/lib/markdownUtils.js
|
||||
packages/lib/markdownUtils2.test.js
|
||||
packages/lib/markupLanguageUtils.js
|
||||
packages/lib/migrations/42.js
|
||||
packages/lib/mime-utils.js
|
||||
packages/lib/models/Alarm.js
|
||||
packages/lib/models/BaseItem.test.js
|
||||
packages/lib/models/BaseItem.js
|
||||
@@ -921,6 +965,7 @@ packages/lib/ntp.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
packages/lib/reducer.test.js
|
||||
packages/lib/reducer.js
|
||||
packages/lib/registry.test.js
|
||||
packages/lib/registry.js
|
||||
@@ -1203,10 +1248,12 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/types.js
|
||||
packages/lib/urlUtils.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/focusHandler.js
|
||||
packages/lib/utils/frontMatter.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
@@ -1217,7 +1264,8 @@ packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/joplinCloud/index.js
|
||||
packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
@@ -1289,6 +1337,7 @@ packages/renderer/MdToHtml/rules/mermaid.js
|
||||
packages/renderer/MdToHtml/rules/sanitize_html.js
|
||||
packages/renderer/MdToHtml/rules/source_map.js
|
||||
packages/renderer/MdToHtml/rules/tableHorizontallyScrollable.js
|
||||
packages/renderer/MdToHtml/rules/utils/defaultRule.js
|
||||
packages/renderer/MdToHtml/setupLinkify.js
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/assetsToHeaders.js
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
BuildAndroidDebug:
|
||||
AssembleRelease:
|
||||
needs: pre_job
|
||||
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -25,6 +25,11 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '20'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -40,7 +45,9 @@ jobs:
|
||||
- name: Install
|
||||
run: yarn install
|
||||
|
||||
- name: Build Android Release
|
||||
- name: Assemble Android Release
|
||||
run: |
|
||||
cd packages/app-mobile/android && ./gradlew assembleDebug
|
||||
cd packages/app-mobile/android
|
||||
sed -i -- 's/signingConfig signingConfigs.release/signingConfig signingConfigs.debug/' app/build.gradle
|
||||
./gradlew assembleRelease
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.1
|
||||
uses: contributor-assistant/github-action@v2.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
||||
@@ -10,7 +10,6 @@ _vieux/
|
||||
!var/sessions/.gitkeep
|
||||
!var/SymfonyRequirements.php
|
||||
.DS_Store
|
||||
.vscode/*
|
||||
*.map
|
||||
*.pro.user
|
||||
*.sublime-workspace
|
||||
@@ -271,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
@@ -302,6 +302,8 @@ packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
@@ -433,6 +435,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
@@ -441,6 +445,7 @@ packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
@@ -481,20 +486,23 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
@@ -562,11 +570,14 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
@@ -579,26 +590,38 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
@@ -639,7 +662,9 @@ packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
@@ -670,6 +695,7 @@ packages/app-mobile/utils/fs-driver/tarExtract.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -722,8 +748,9 @@ packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
@@ -736,8 +763,22 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/util/setupVim.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
@@ -763,6 +804,7 @@ packages/generator-joplin/generators/app/templates/src/index.js
|
||||
packages/generator-joplin/tools/updateCategories.js
|
||||
packages/htmlpack/src/index.js
|
||||
packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
packages/lib/BaseApplication.js
|
||||
packages/lib/BaseModel.js
|
||||
@@ -852,6 +894,7 @@ packages/lib/markdownUtils.js
|
||||
packages/lib/markdownUtils2.test.js
|
||||
packages/lib/markupLanguageUtils.js
|
||||
packages/lib/migrations/42.js
|
||||
packages/lib/mime-utils.js
|
||||
packages/lib/models/Alarm.js
|
||||
packages/lib/models/BaseItem.test.js
|
||||
packages/lib/models/BaseItem.js
|
||||
@@ -901,6 +944,7 @@ packages/lib/ntp.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
packages/lib/reducer.test.js
|
||||
packages/lib/reducer.js
|
||||
packages/lib/registry.test.js
|
||||
packages/lib/registry.js
|
||||
@@ -1183,10 +1227,12 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/types.js
|
||||
packages/lib/urlUtils.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/focusHandler.js
|
||||
packages/lib/utils/frontMatter.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
@@ -1197,7 +1243,8 @@ packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/joplinCloud/index.js
|
||||
packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
@@ -1269,6 +1316,7 @@ packages/renderer/MdToHtml/rules/mermaid.js
|
||||
packages/renderer/MdToHtml/rules/sanitize_html.js
|
||||
packages/renderer/MdToHtml/rules/source_map.js
|
||||
packages/renderer/MdToHtml/rules/tableHorizontallyScrollable.js
|
||||
packages/renderer/MdToHtml/rules/utils/defaultRule.js
|
||||
packages/renderer/MdToHtml/setupLinkify.js
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/assetsToHeaders.js
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.enabled": true
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
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 0f52b73c61625db2a3081c0950b6bdd2b06e3d40..b0fc3de4be0b3a26b638683613c63c783c2739bb 100644
|
||||
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
|
||||
@@ -38,7 +38,7 @@ import com.facebook.react.uimanager.common.ViewUtil;
|
||||
@@ -37,7 +37,7 @@ import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
@@ -11,7 +11,7 @@ index 0f52b73c61625db2a3081c0950b6bdd2b06e3d40..b0fc3de4be0b3a26b638683613c63c78
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -151,7 +151,10 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
@@ -149,7 +149,10 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec
|
||||
}
|
||||
|
||||
private class ConcurrentOperationQueue {
|
||||
@@ -0,0 +1 @@
|
||||
<svg viewBox="-253.3 -97.4 506.4 422.6" width="506.4" height="422.6" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M253.1,325.2l0-422.6l-506.4-2.842170943040401e-14l0,422.6l506.4,0" fill="#ffffffa8" class="js-draw-image-background"></path><path d="M253.1,325.2l0-422.6l-506.4,0l0,422.6l506.4,0" fill="#ffffff6d"></path><path d="M253,325.8L253,-113.4L6.4,-113.4L6.4,325.8L253,325.8" fill="#ffffff6d"></path><image href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 184 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="-1.816785937500157 -2.056489453124989 545.4634263671875 435.06855617557267" width="545.4634263671875" height="435.06855617557267" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" class="js-draw--autoresize"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M543.6466404296873,433.0120667224477L543.6466404296873,-2.056489453124982L-1.816785937500157,-2.056489453124989L-1.816785937500157,433.0120667224477L543.6466404296873,433.0120667224477" fill="#ffffffa8" class="js-draw-image-background"></path><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZwAAAKYCAYAAABHOE6yAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAAtdEVYdENyZWF0aW9uIFRpbWUAV2VkIDE1IE1heSAyMDI0IDEyOjQ1OjIzIFBNIFBEVGxXaYMAACAASURBVHic7N13eBTV3sDx72zJbnrvvSfU0HuvgoCKvYCiWLD3jmJDsV71YgUFAem9hN5LKCEQSEJNCISE9LZJts28f2xYiQEFhKj3PZ/nySPOzpxzZnb2/OaUmZGKikqUSZM+RBAEQRCuJw1AWXnl310OQRAE4X+cBmDP/sy/uxyCIAjC/zgNgEql+rvLIQiCIPyPE5FGEARBaBIi4AiCIAhNQgQcQRAEoUmIgCMIgiA0CRFwBEEQhCYhAo4gCILQJETAEQRBEJqESpKkv7sMgiAIwv8DooUjCIIgNAkRcARBEIQmIQKOIAiC0CQ0f3cBBEEQhKbj4+1Jj+5d0et1nDiRze69+5ssbxFwBEEQ/p8IDgzgk4/eITw8FJVKRU1NLd98N4V5C5c1Sf7XvUutRbN4miXEiSdSC4Ig/M3uufs24uNj0ev1ODg44OHhzj13305cbFST5H/dWjiSJNGnd3ceeuBeamvrmPDeJHJP512v7ARBEIQ/4e7m1miZXq/Dzc21SfK/LgFHkiQGD+rHc089hre3F4qiEBEeKgKOIAjC3yjvbH6jZSUlZZw5c7ZJ8r8u/VzdunTk2acetQebtAPpHM48ej2yEgRBEC7TrNnzOZyRhdFoxGQyU1FRyYxZcyg4V9Qk+V/TFo4kSfTo1plXX34GH29vANIOpPPRJ19SUlJ6LbP6WwQHBSDLMvkFhX93UQRBEK5YeUUlDz3yNP379sTJyYn0Q4fJOnqiyfLXKIpyTRKSJIkbBvXj+Wcex9PTA4CMzCzem/gp2Tm51ySPy9Gze2fcXF3YsHk7NTW19uWdOrTF29uTlcnr7cscHfV06tAGlUrFhk3b/zTt5595nNraOsZPmIhVlq9L+a8VrUZD+3atcXNzZfXaTX93cQRB+IcwmkysSF73t+R9TVo4kiQxdMhAXn/lOXQODgCsXb+RtyZ8RJ3ReC2yuGy9enSjX99eVFRWsXV7CmBrmTwxbizNmyVwMD2DM3m2fkx/P1+ee+ZxDqSlX1bAiYgIw2CogX/B8+ccdA7ccvMwoiIiRMARBOEf4S8HHEmS6NqlA48/MsYebA4cPMSUn2Y2ebAByDxyhBsG98ffz8++LMDfD29vTwDat21tDziOej2urq7sSU1r8nI2Bdkqc61asIIgNBQSHIiHh/sfrlNdVU3u6Tzkf9jvMCQ4EAcHLQUFRdTU1v75BtfIXwo4kiQxeGBfnn36MfuYTdaRY0yc9AVHjzVdv+CFNm/ZybNPP05gYAAqlQpFUQgIDEBCoqysnMTEBBYvSwYgNjYaFEg/lAmAWq2ifdskWrdqgaSSOJWTy+p1my5ZaatUKpolxtG+XRucnZwoLCpi0+btFBWX2NdxcNAyeGBfggIDsVqtHDl6jO0792C1WpEkicSEOOJjo9m1ew99+/TCzdWVsvJy1q7biEqtplf3rvj5+VJVVc2mLds4/bvZJB3ataZ161Zo1Gpyck6xdfsuDDW1KIqCLMvIyj+7608Q/q3atW3Fww+OvuTnsqKwcNEyps+cB1ZrE5bs0jQaNaPuvZNePbqg0+k4cTKHGTPnknnkWNPk/1c27tmjC889PQ5vby8AzGYzFRUVPPLQaMBWSf9WVyu2JcqFy3+ryBVFYc/e/SxelozFYrnqMhUVl5B35iyxMZF4uLtRXW2gY7skzp7Nx1BTQ2xMFN5enpSUltGtS3uKioqoqalFrVYz8qYbefSRB5BlGbPJjJubG506tWfCex9fNK8eXTvyxmsvotFoKK+owM/Xh6E3DGDsY89hNpvx8HDjo/ffomWLZlRUVKHTOaDVapgxay4/TZ+NxWKhfbsk7h91F2NrR+Hg4IBKrcLZyYkbBvXHYDAQHxeL2WLBw92dEcMGc+d9Y7FYbCfv/ffdyZj776GuzohVlnFzdWHu/MV89+M0AKyyjCL/s66sBOF/xcrkDYwdM5qgoMCLfl5cXEJG5lGs/5BgAzBi2A08+vADaNRqAGJjonF3c+Wl1yY0GPO+Xq464Hh7eTJm1N32YAOg1Wrp1LH9VRemR/du7EzZS97ZgqtOA+DQ4cO0aZOEu7sbdUYjLZonsnzlGs7k5fHs0+OIjAijpLSMmOgYMrOOUG0wEBEeyv2j72bL1h28/+FnaDQannp8LLffejMbN21ly7ZdDfJwdXHmxRee4mR2Dm+/N4lz5wrp06s7r7z4DC8+O45Jn/2Xe+68jZYtmvPV5O+ZN38JGo2Gl198iltvGcHe1APsT0tHpZJwdXFhztyFTJ02C71Ox7tvv0qP7l3ZtmMXt989hsqqKu6+YySPjL2f224Zxq9zF9O1cwcee2QM6zZs4rMvJlNSWs7TTzzM0BsGsjJ5HWfyziJbraKFIwjXidlsZvrM2bzw7JNoNI2r0r2p+0k/lPE3lOzSWrZobg82YOulioqKIC4mirSDh697/lcdcCwWC/kFBcQnxOGg1QIgyzJnz+ajKIrtD35r0Vzw70upqanBav3rFeS6jVvp3asnvj7eeHq44+jkxMFDGaQfyuSJcQ8TGRlOUXExvr4+LFi8jJqaWoYO7o+LsxP5+QUMGtAHgLq6OgA6tG/bKOAM6NcLL09Pfpo2i/z8cwDsStnHnHmL0Ot1BPj70rFDG44eO8bc+UuwWq1YrFZ+mPILHdq1YUC/XuxPSwfAYKhhys+zsFgsmM0WNm/ZTscO7di0eTslpWUAHDl6nOpqAzExUUiSRM8eXdFoNJw7V0iXzh0AqK42oNfr6di+DafPnMVqtSKLFo4gXDcbNm5j0IC+JLVuxYUvs6ysrGLpslXU1Nb9jaVrrLKystGyGkNtk43jXHXAqayqZvrMOURFRRITbXsOjyzLLFm2kqwjx5FlK1ZZRrbKyIqCIsvIsnK+Y60RRVEoLSvnXOFfvwEpOycXs9lMdFQEYaHBlJeXU1JSislkIu9MHokJcegcNMiyTGFhEYqiEB8Xg5OTE6Pvu8s+wCchIUkS7u6NHwcRHRWB2Wyh+ILxGkNNDb/MmodKJRERHoq3lxfbd+5q0KSuqa3lXGEhMdGR9mWy3HBwv7auDqtVxmQ225dZ61srWo0GtVqNr483kiRxx2232I+oSpJwcHAgIjwUUGxdaqKFIwjXTWlZOevWb6FZYiI6nYN9edqBg+zZd+BvLNnFbd26g9tvvQkHh9/KeuDgIU5mn2qS/K864CiKQkbmMd6f+AnvvfMmQYEBaDQa7h91N6++8Q579qXZxxqamrHOSH5BAe3bJREaGkJG5lF7N932nbsZNnQwvt5eVFZWkZt7xr5dSWkp27btxGAwNEgvM6vxUxJkRQEJfv+KbpPJBNgac4qioFapG3wuASpJhdlq5mrZspSoMxpZtHgZ8u/uCTqYfhhFVqisrKK8vOKq8xEE4Y/Jssyu3XsZeXYYUZERAFQbDHz2xeR/1NjNeXtSD/DcC69x15234eLiTHr6Yb76ZkqT1dV/eVr0gfRMxr/9Ps8/+wSJCfE4Ozvz/LNP8OXX37F5685GlWFTMBhqOJxxhG5dO+Hn68OSpSsx17cWMo8c4847RuLs7ERefgE59QHn6LETxMZGs3jpKtIPZ9rT8vfzvWirKyvrKLfeMgJ//9+mX+v1evr16YFWqyVl9z4Ki4pp3iwBrUaDuX4ihIuLMwGB/qxdt/Gq989stlBUXIxWo2X12k328qokCRcXZww1tciyzIJFy3BycrzqfARB+HPZObmsWbuBRx8eg6IorFmzgdwmejbZ1diRso+du1MBmvy2iWvyLLW0gxl89p/JnDiRDUBYaAjPPT2Oju2TrkXyV8xoMnH8RDY+3t4oisK+/b81bUtKSqmsrCQgwJ/skzn2FsmK5HXIssIjY0cTGOCHSqWiR7dOfPHJ+/Tv27NRHhu37KCgoJDBA/sRGRGGSqWiX+/uPP3kI8TFRlFYVMyu3XsJDw/ngdF34eiox9PDnSfHjUVCYsPGbX9pH7dt34ksW3nskTH1XWjQpXN7Pnj3DRLiY5AkiaDAAJolxP2lfARB+HOz5y0mP7+A4uISVq3+e+7ivxLnx9mb2jV50oCiKKTuT2fipM+ZNHECXl5eBAcHMWniBF569S12701r8pZOYWERVdVVFBeXknXkuH352fxzZGYdIzoqkrSD6fbl2Tm5/DB1Oo+OfYD5s6dRU1uLoLine truncated
|
||||
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="174.4 -53.8 359.6 318.9" width="359.6" height="318.9" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M534,265.1l0-318.9l-359.6,1.4210854715202004e-14l0,318.9l359.6,0" fill="#ffffffa8" class="js-draw-image-background"></path><path d="M534,265.1l0-318.9l-359.6,0l0,318.9l359.6,0" fill="#ffffff6d"></path><image href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="-330 -170 306.6375 187.43578125" width="306.6375" height="187.43578125" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" class="js-draw--autoresize"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M-23.3625,17.43578125l0-187.43578125l-306.6375,0l0,187.43578125l306.6375,0" fill="none" class="js-draw-image-background"></path><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZgAAACnCAYAAAAsYEqZAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAAtdEVYdENyZWF0aW9uIFRpbWUAV2VkIDE1IE1heSAyMDI0IDA5OjQ4OjMxIEFNIFBEVMzUBoEAACAASURBVHic7d13dFT3nf//51SNRr0ihFBBFIuOKKIjwPRi2oIL4BL35OtN4k3263yzjrP5OXHseL1JvGbtQHBijG2w6YhmqmmmWcKAAAn13ttI0+7M7w8ZGYxkBNZoBng/zuEczsy9V5+R5t7XvZ+q+ueaj5wIIYQQHUxVWFgoASOEEKLDqd1dACGEEHcnCRghhBAuIQEjhBDCJSRghBBCuIQEjBBCCJeQgBFCCOESEjBCCCFcQgJGCCGES0jACCGEcAkJGCGEEC4hASOEEMIlJGDEPcFisZCRcdndxWjT7t27qaurc3cxhOhQEjDirme32/nqqzOcOXOG1NSv3F2cG+zevZtXX32V3/zmN5hMJncXR4gOIwEj7noqlYqIiK7odDquXLnCmTNnsFqt7i4WiqKwceNGVqxYgcPhYN68eWg0GncXS4gOIwEj7noajYaoqCj69u2H3W4nJyeby5cvubtY7N27lzVr1lBRUcFTTz1FUlISBoPB3cUSosNoO+Igp0+fxmAwcN9998kdmPBIWq2WPn36oNNpOXXqFOnp6dhsdoYMGeKW8uzcuZM33ngDq9XKs88+y4MPPijnjrjr3DRgampquHDhAnl5eTgcDoKDg+nfvz/dunVDpVIBsGnTJgB+/etfy0kiOpXD4cBqtVJfX9+u7X19fYmM7EZRUSGZmRnodDruu+8+tNoOude6KUVR2LdvHytWrMBqtTJw4EB69erF+fPn27V/bGwsRqOx08orPJ/D4aC8vJy0tDQqKysBCA0NZdCgQYSGhqJWu6+i6ntXtKyqquKtt96itraWXr16odVqycvLo66ujmeeeYaBAwcC8B//8R9Ac8B4eXl1TsnFPa++vp68vFwqKiopKSm+rWPodDoGDRpMfHx8B5eudV999RWvvvoqpaWlt7X/+PHj6dOnD5MnT6Zbt24dXDpxp3E6naSnp/Puu+9iMBiIiYkBIDc3F0VReOaZZ+jVq5fbytfmbZDD4eDPf/4zBQUFvPzyy0RFRaFSqWhoaOD111/nv/7rv/jLX/6Cv79/Z5ZXCKA5XPbv34fFYsHhcNz2cXQ6Xad+hyMjIwkNDb3tgDl06BDHjh1j06ZNrFixgi5dunRwCcWdxG63s3//fnx8fHjppZda2vDMZjNvvvkmO3fuJDY2Fp1O55bytRkwmZmZHDlyhFdeeYW4uLiW1wMDA3nsscf4wx/+QH5+Pv369bth38bGRlauXElKSgpeXl40Njby4IMPsmzZMvR6PQB1dXW88cYbnD59Gp1Oh6IoPPfcc8ycOROVSoXT6SQnJ4cXX3wRm82GoiiEhobyxhtvyEl1D3M6nRQXF3PixJdYrVYMBgPjxo0nKCioXfuXlBRz8OBBAIxGH8aMGUNwcLAri3ydLl268J//+Z/87ne/IzU1lYSEBH79618THR3drv1ramp46qmnqKioYMmSJbz22muMHDnSxaUWnsrhcFBfX09sbCx+fn4tr+t0OqZPn87Jkyex2+1oNBpOnDjBa6+9htPpRFEUjEYjb731Ft26dePcuXM8//zzvPvuuyQkJABQWVnJq6++yqxZs5g8eTJNTU2sWLGCXbt24eXlRUNDAy+99BKTJ09usxpO8+KLL77S2hsXLlxg//79LF269IaT19fXl6SkJLp27Yper2f//v1A8+O7Vqtlz549HD16lMWLFzN//nxiY2PZuHEjGo2GhIQE1Go1GzZsIDU1laeffprZs2ej0+k4ePAgI0aMwNfXl/z8fF577TUGDRrE8uXLGTlyJDk5OVRUVNC/f3+pg75HWSwW0tLSqK2tJSQkhJEjR7U7XIqLizl9+jQ2mxWj0YchQ4a45WbFx8eHuLg4MjIyuHjxIqWlpYwYMaJd1cteXl4kJSVRV1dHZmYmFRUVJCUl4e3t3QklF55GpVJRUlLCoUOHCA4OJjAwEL1ej0qlIioqiqSkJHQ6HcXFxbz99tsMHz6cJ554gqSkJHJzc/nyyy8ZP348fn5+pKSkEBoaSv/+/VGpVOTk5LB3714WLlyI0Whky5YtpKWlsWzZMmbPnk1QUBBr1qwhMTGR0NDQVsvX5lW6uroap9OJ0Wi84T0vLy+ioqJa3U9RFD7++GNmzJjB3Llz0Wg0DBgwAICNGzcybtw4IiMjOXToEPHx8UyYMAGdTkf37t3p0aMHVqsVRVHYvn07Go2G559/vqUMRqORd955h9mzZ7f588XdraKinIqKcgBGjRqNj49Pu/YrLCzkzJnTNDY2YjQaGTFihFufhBMSEnj55Zf5/e9/z5EjR3jhhRf47//+bwIDA793P5VKRUxMDMuWLWP37t2cP3+ec+fOMX78+E4qufAkGo2GqVOnYrVaeeONNwgPDycpKYnk5GTi4+NbOmIZDAaWLFlCYmIivr6+AFitVv74xz9SWVlJVFQUCxYs4MyZM8ydOxdfX18yMjKIjY0lLCyMqqoqdu3axaOPPsrYsWNRqVT069ePEydOsHPnTnr37t3qU0yb3QucTidOZ5vt/20qKCjAYrEwcODAlh5lGo2GUaNGUV9fT25uLgBTp07l0qVLbN68mezsbPR6PdOnTyc6OpqmpiYKCgrw8/Nj//79bN++ne3bt3P58mVKS0upra295XKJu0NFRQWKopCQkNDucKmrq+Ps2TQaGxsxGLxJTEz0iGrW7t2782//9m/06dOHK1eu8Nprr9HY2NiufWNjY1m6dClWq5WzZ8/+oHYocWcLDAxk6dKlrFq1ipkzZ3L06FFeeOEF/vSnP1FRUQFAUFAQw4YNIycnh927d7Nr1y4uXryI0+nEbrcDMHnyZCorKykrKwMgNTWVXr164eXlRWlpKUVFReTk5JCSksL27dv5/PPP8fX1pbS0FLPZ3GrZ2nyCCQgIQK1W09TUdMN7NpuN2tpafH19bxgYZjab0ev1N7zu4+ODoigtx5s9ezaBgYEcP36cY8eOYTQaSUxMZNasWSiKgqIo1NbWcu7cueuOM2HCBOlYcA+zWCwA+Pm1/zvg4+NDz549ycjIYMCAgXTr5jlPv/Hx8fz85z9n9erVLF26tNUag7b07t3bhSUTd5rQ0FDmz5/P9OnTuXTpEitWrOCTTz7h6aefpra2lpUrV1JTU0NUVBT+/v7U1NRct39UVBQRERGkp6fj7+9PVlYWixcvRq1WtwRIZmbmdd/RyMhI4uPj2xye0mbAXN3p7NmzREdHX/f4k5qaytq1a/npT3/a0i3u2g95tV92jx49Wh7RCgoK0Ov1hIWF0dTURFZWFgMGDGDUqFGYTCbOnj3LmjVriIqKon///hgMBvz8/PjZz37WUvirT1XS/nLvMpma7/BvpTu8RqOhR494wsO7EBAQ4Kqi3ba+ffvyq1/9qt1tSUJcVVdXx9tvv828efPo27cvAN7e3gwePJiJEydy9OjRlqfcoqIifvGLX7T0CP7yyy85cOBAy7FUKhVJSUmkp6fT1NREdHR0y/U9ICCAgIAAFi5ceF3HLofDgVqtbjNg2qwii4yMpF+/fmzbtq2lPcbpdGKxWEhJSaGwsLDV+uKQkBDCw8M5e/Ysdru95RHswIED+Pj4EBkZiclk4mc/+xlHjx5Fr9cTHBzMgAEDMBqNVFVVYTAYiI2NZd++fZSVlaFWq1Gr1Vit1pb+3eLeFBzcfBG+2g7TXhqNxiPD5arbCZfbqcIWdxeVSkV+fj6nTp1CUZSW67SiKNTV1eHt7d0yvESj0bRUK9tsNi5fvnF28V69epGTk8OmTZtITExseVoJDQ0lODiYtLQ0ANRqdcvPvjq4szVtPgpoNBpeeuklfv/73/OnP/2JAQMGoNFoyMzMpKCggF/+8pdtnrBLlixh1apVKIpCREQEubm5pKWl8dxzzxESEgLAsmXLSElJoaKigpCQEC5fvozVam3pwTBr1izS09P57W9/y6RJk9DpdKSnp+Pj48OTTz4p1WT3KKOx+QQpKioiIaGv2/r3u5vFYmHnzp0Aci7cw3x8fHj44Yf54IMPKCwsJDY2FrVaTVFRESdOnODxxx/HYDCQkJDAJ598wjvvvEOPHj0oLy9vaYO5Wu0MzdVkBoOBwsJCRowY0fJ6YGAgs2fP5v3332+pZquqquLYsWMsX76c8PDwVsvXZjdlaO6OPGTIEDQaDVVVVZjNZiIjI1m4cGFLEAA0NDQQFxdHz549UavVdOnShfj4eCorK6mpqWl5tEpMTGzZp3fv3gQHB1NRUUF1dTWhoaEsXLiQuLg4VCoVPj4+LQlaXl6OyWSiZ8+ezJ07V6oS7mFarZaCggJMJhNeXl5tdo+8m1ksFlavXk1KSgrR0dEsXbr0nvw9iOLine truncated
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="-207.9615496339302 462.0390217320611 292.68421875 392.73609375" width="292.68421875" height="392.73609375" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M84.72266911606982,854.7751154820612l0-392.7360937500001l-292.68421875,0l0,392.7360937500001l292.68421875,0" fill="#180a0a" class="js-draw-image-background"></path><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA4UAAAS5CAYAAABrz774AAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAAtdEVYdENyZWF0aW9uIFRpbWUAV2VkIDE1IE1heSAyMDI0IDEwOjU3OjI1IEFNIFBEVEAVgDkAACAASURBVHic7N1pcxtXni74JzORiZ0EuFNcxU2SJcuWbNmSXZbtai9dtru7rvve6TsTPTMxE/NiPsaNmJiPMG9udPSNie6Znujbt5Zu21Vlu2xLslySLWuzZMmkSHEVVyzEjgQyc16AoAAC4IqNyOcXwZCYTACHAIjMJ885/yOkUikDREREREREZDqKokCsdSOIiIiIiIioVgSGQiIiIiIiIrMSGAqJiIiIiIjMSxQkhkIiIiIiIiKzssDCUEhERERERGRWEnsKiYiIiIiIzEs2FFhu3LhR63YQERERERFRDVgNKyzTk5O1bgcRERERERHVgGJYOXyUiIiIiIjIrKyQGQqJiIiIiIjMSgF7ComIiIiIiEyLoZCIiIiIiMjEJHBJCiIiIiIiItNSoDAUEhERERERmRVDIRERERERkYnJHD5KRERERERkXhZYGAqJiIiIiIjMiqGQiIiIiIjIxCSIDIVERERERERmJUJgKCQiIiIiIjIrrlNIRERERERkcgyFREREREREJsXho0RERERERKbGUEhERERERGRaAkMhERERERGReTEUEhERERERmZgOg6GQiIiIiIjIzBgKiYiIiIiITMpgTyEREREREZF5CRAZComIiIiIiMyKPYVEREREREQmx1BIRERERERkUqw+SkREREREZGIadIZCIiKqjUAgiEQiUetmHDqJRAKBQLDWzSAiogZiqXUDiIjIfAKBIBYWnkCURAwdHYTNZqt1kw6FRCKBqcfT0DUdAOD1emrcIiIiagTsKSQioqrKBkIA0DUdU4+n2WO4C1sD4cLCE/YYEhHRgXFOIRERVZWm61hcWsrbxmC4s62BMGtxaQmarpe4FRER0c4MsKeQiIiqSBIzw0VFKf/ww2BYWqlAmB16K4k8lBMR0f4JEBgKiYioumw2G4PhLu0UCDkXk4iIDkpn9VEiIqoFBsOdMRASEVE1GJxTSEREtcJgWBoDIRERVYuAep9TaGhIpTiBnoioUTEY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 truncated
|
||||
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 310 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="-250 -200 678.5 506.7" width="678.5" height="506.7" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" class="js-draw--autoresize"><style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style><path d="M428.5,306.7l0-506.7l-678.5,0l0,506.7l678.5,0" fill="none" class="js-draw-image-background"></path><image href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -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://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.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://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://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.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>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
# For development this compose file starts the database only. The app can then
|
||||
# be started using `yarn start-dev`, which is useful for development, because
|
||||
# it means the app Docker file doesn't have to be rebuilt on each change.
|
||||
#
|
||||
# Note that log is setup to give as much information as possible, including
|
||||
# whether it's the master or slave database that is being used for a query.
|
||||
#
|
||||
# To setup and test replication, use the following config in Joplin Server. Note
|
||||
# in particular the different port, which means we access the slave and not the
|
||||
# master.
|
||||
#
|
||||
# DB_USE_SLAVE=true
|
||||
# SLAVE_POSTGRES_PASSWORD=joplin
|
||||
# SLAVE_POSTGRES_DATABASE=joplin
|
||||
# SLAVE_POSTGRES_USER=joplin
|
||||
# SLAVE_POSTGRES_PORT=5433
|
||||
# SLAVE_POSTGRES_HOST=localhost
|
||||
# USERS_WITH_REPLICATION=ID1,ID2,...
|
||||
|
||||
version: '3'
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
command: postgres -c work_mem=100000
|
||||
|
||||
postgresql-master:
|
||||
image: 'bitnami/postgresql:16.3.0'
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- '5432:5432'
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_DB=joplin
|
||||
|
||||
# Use this to specify additional Postgres
|
||||
# config parameters:
|
||||
#
|
||||
# command:
|
||||
# - "postgres"
|
||||
# - "-c"
|
||||
# - "log_min_duration_statement=0"
|
||||
- POSTGRESQL_PASSWORD=joplin
|
||||
- POSTGRESQL_USERNAME=joplin
|
||||
- POSTGRESQL_DATABASE=joplin
|
||||
|
||||
- POSTGRESQL_REPLICATION_MODE=master
|
||||
- POSTGRESQL_REPLICATION_USER=repl_user
|
||||
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
|
||||
|
||||
- POSTGRESQL_LOG_HOSTNAME=true
|
||||
- POSTGRESQL_PGAUDIT_LOG=READ,WRITE
|
||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||
|
||||
postgresql-slave:
|
||||
image: 'bitnami/postgresql:16.3.0'
|
||||
ports:
|
||||
- '5433:5432'
|
||||
depends_on:
|
||||
- postgresql-master
|
||||
environment:
|
||||
- POSTGRESQL_REPLICATION_MODE=slave
|
||||
- POSTGRESQL_REPLICATION_USER=repl_user
|
||||
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
|
||||
- POSTGRESQL_MASTER_HOST=postgresql-master
|
||||
- POSTGRESQL_PASSWORD=joplin
|
||||
- POSTGRESQL_MASTER_PORT_NUMBER=5432
|
||||
|
||||
- POSTGRESQL_LOG_HOSTNAME=true
|
||||
- POSTGRESQL_PGAUDIT_LOG=READ,WRITE
|
||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"./packages/renderer/**/node_modules/": true,
|
||||
".eslintignore": true,
|
||||
".gitignore": true,
|
||||
".vscode/*": true,
|
||||
".yarn/cache": true,
|
||||
".yarn/install-state.gz": true,
|
||||
".yarn/plugins": true,
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"postinstall": "gulp build",
|
||||
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
|
||||
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
|
||||
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@17/bin:$PATH\" node packages/tools/release-android.js",
|
||||
"releaseAndroidClean": "node packages/tools/release-android.js",
|
||||
"releaseCli": "node packages/tools/release-cli.js",
|
||||
"releaseClipper": "node packages/tools/release-clipper.js",
|
||||
@@ -86,7 +86,7 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.2.0",
|
||||
"lint-staged": "15.2.2",
|
||||
"madge": "6.1.0",
|
||||
"npm-package-json-lint": "7.1.0",
|
||||
"typescript": "5.2.2"
|
||||
@@ -104,11 +104,11 @@
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch",
|
||||
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",
|
||||
"chokidar@^2.0.0": "3.5.3"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -63,7 +63,7 @@
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"terminal-kit": "3.0.1",
|
||||
"terminal-kit": "3.0.2",
|
||||
"tkwidgets": "0.5.27",
|
||||
"url-parse": "1.5.10",
|
||||
"word-wrap": "1.2.5",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~3.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.14",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -49,6 +49,8 @@ describe('MdToHtml', () => {
|
||||
checkboxRenderingType: 2,
|
||||
},
|
||||
};
|
||||
} else if (mdFilename.startsWith('sourcemap_')) {
|
||||
mdToHtmlOptions.mapsToLine = true;
|
||||
}
|
||||
|
||||
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="joplin-table-wrapper">
|
||||
<table class="maps-to-line" source-line="0" source-line-end="3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>This</th>
|
||||
<th>is</th>
|
||||
<th>a</th>
|
||||
<th>test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>This</td>
|
||||
<td>table</td>
|
||||
<td>has</td>
|
||||
<td>line numbers</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="maps-to-line" source-line="4" source-line-end="5">When <code class="inline-code">mapsToLine</code> is enabled for this file, the table above should have line numbers that link to the original markdown.</p>
|
||||
@@ -0,0 +1,5 @@
|
||||
| This | is | a | test |
|
||||
|------|----|---|------|
|
||||
| This | table | has | line numbers |
|
||||
|
||||
When `mapsToLine` is enabled for this file, the table above should have line numbers that link to the original markdown.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Not a task
|
||||
---
|
||||
|
||||
This is a note.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Task
|
||||
completed?: yes
|
||||
---
|
||||
|
||||
This is a test. This task should import as completed.
|
||||
@@ -26,6 +26,10 @@ export interface AppStateDialog {
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EditorScrollPercents {
|
||||
[noteId: string]: number;
|
||||
}
|
||||
|
||||
export interface AppState extends State {
|
||||
route: AppStateRoute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -34,8 +38,7 @@ export interface AppState extends State {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
windowContentSize: any;
|
||||
watchedNoteFiles: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
lastEditorScrollPercents: any;
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
devToolsVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
|
||||
@@ -204,7 +204,7 @@ class Application extends BaseApplication {
|
||||
public updateEditorFont() {
|
||||
const fontFamilies = [];
|
||||
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
|
||||
fontFamilies.push('Avenir, Arial, sans-serif');
|
||||
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
||||
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
@@ -420,6 +420,7 @@ class Application extends BaseApplication {
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
reg.setDispatch(this.dispatch.bind(this));
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
|
||||
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
|
||||
@@ -243,6 +243,24 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) {
|
||||
const goToJoplinCloudLogin = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'JoplinCloudLogin',
|
||||
});
|
||||
};
|
||||
settingComps.push(
|
||||
<div key="connect_to_joplin_cloud_button" style={this.rowStyle_}>
|
||||
<Button
|
||||
title={_('Connect to Joplin Cloud')}
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={goToJoplinCloudLogin}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
settingComps.push(
|
||||
<div key="check_sync_config_button" style={this.rowStyle_}>
|
||||
<Button
|
||||
@@ -596,6 +614,9 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
size={ButtonSize.Small}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ interface Props {
|
||||
function manifestToItem(manifest: PluginManifest): PluginItem {
|
||||
return {
|
||||
manifest: manifest,
|
||||
installed: true,
|
||||
enabled: true,
|
||||
deleted: false,
|
||||
devMode: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
@@ -83,6 +83,7 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
|
||||
|
||||
output.push({
|
||||
manifest: plugin.manifest,
|
||||
installed: true,
|
||||
enabled: setting.enabled,
|
||||
deleted: setting.deleted,
|
||||
devMode: plugin.devMode,
|
||||
@@ -213,8 +214,11 @@ export default function(props: Props) {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
}, [pluginService, props.onChange]);
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettings, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
const pluginSettingsRef = useRef(pluginSettings);
|
||||
pluginSettingsRef.current = pluginSettings;
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettingsRef, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettingsRef, repoApi, onPluginSettingsChange, true);
|
||||
|
||||
const onToolsClick = useCallback(async () => {
|
||||
const template = [
|
||||
|
||||
@@ -40,7 +40,10 @@ export default function(props: Props) {
|
||||
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
|
||||
const [searchResultCount, setSearchResultCount] = useState(null);
|
||||
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, props.repoApi, props.onPluginSettingsChange, false);
|
||||
const pluginSettingsRef = useRef(props.pluginSettings);
|
||||
pluginSettingsRef.current = props.pluginSettings;
|
||||
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, pluginSettingsRef, props.repoApi, props.onPluginSettingsChange, false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchResultCount(null);
|
||||
|
||||
@@ -9,4 +9,25 @@
|
||||
p {
|
||||
padding: calc(var(--joplin-font-size) * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.joplin-cloud-account-information {
|
||||
margin-bottom: var(--joplin-margin);
|
||||
|
||||
table {
|
||||
margin-bottom: var(--joplin-margin);
|
||||
border: none;
|
||||
|
||||
td {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--joplin-border-color4);
|
||||
padding: var(--joplin-font-size);
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,14 @@ 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';
|
||||
|
||||
type JoplinCloudConfigScreenProps = {
|
||||
inboxEmail: string;
|
||||
joplinCloudAccountType: number;
|
||||
userEmail: string;
|
||||
joplinCloudWebsite: string;
|
||||
};
|
||||
|
||||
const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
|
||||
@@ -17,8 +21,29 @@ const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
|
||||
|
||||
const isEmailToNoteAvailableInAccount = props.joplinCloudAccountType !== 1;
|
||||
|
||||
const goToJoplinCloudProfile = async () => {
|
||||
await bridge().openExternal(`${props.joplinCloudWebsite}/users/me`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="joplin-cloud-account-information">
|
||||
<h2>{_('Account information')}</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_('Account type')}</strong></td>
|
||||
<td>{accountTypeToString(props.joplinCloudAccountType)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{_('Email')}</strong></td>
|
||||
<td>{props.userEmail}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Button onClick={goToJoplinCloudProfile} title={_('Go to Joplin Cloud profile')}/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{
|
||||
@@ -38,6 +63,8 @@ const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
inboxEmail: state.settings['sync.10.inboxEmail'],
|
||||
joplinCloudAccountType: state.settings['sync.10.accountType'],
|
||||
userEmail: state.settings['sync.10.userEmail'],
|
||||
joplinCloudWebsite: state.settings['sync.10.website'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { clipboard } from 'electron';
|
||||
@@ -9,6 +9,7 @@ import { Dispatch } from 'redux';
|
||||
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import { AppState } from '../app.reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -38,6 +39,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
if (response && response.success) {
|
||||
dispatch({ type: 'COMPLETED' });
|
||||
clearInterval(interval);
|
||||
void reg.scheduleSync(0);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
@@ -77,22 +79,26 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="page-container">
|
||||
<p className="text">{_('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:')}</p>
|
||||
<div className="buttons-container">
|
||||
<Button
|
||||
onClick={onAuthorizeClicked}
|
||||
title={_('Authorise')}
|
||||
iconName='fa fa-external-link-alt'
|
||||
level={ButtonLevel.Primary}
|
||||
/>
|
||||
<Button
|
||||
onClick={onCopyToClipboardClicked}
|
||||
title={_('Copy link to website')}
|
||||
iconName='fa fa-clone'
|
||||
level={ButtonLevel.Secondary}
|
||||
/>
|
||||
{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
|
||||
onClick={onAuthorizeClicked}
|
||||
title={_('Authorise')}
|
||||
iconName='fa fa-external-link-alt'
|
||||
level={ButtonLevel.Primary}
|
||||
/>
|
||||
<Button
|
||||
onClick={onCopyToClipboardClicked}
|
||||
title={_('Copy link to website')}
|
||||
iconName='fa fa-clone'
|
||||
level={ButtonLevel.Secondary}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : null}
|
||||
<p className={state.className}>{state.message()}
|
||||
{state.active === 'ERROR' ? (
|
||||
<span className={state.className}>{state.errorMessage}</span>
|
||||
|
||||
@@ -97,6 +97,7 @@ interface Props {
|
||||
notesSortOrderField: string;
|
||||
notesSortOrderReverse: boolean;
|
||||
notesColumns: NoteListColumns;
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -592,6 +593,13 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onViewJoplinCloudLoginScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'JoplinCloudLogin',
|
||||
});
|
||||
};
|
||||
|
||||
const onViewSyncSettingsScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -684,6 +692,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
} else if (this.props.mustUpgradeAppMessage) {
|
||||
msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage);
|
||||
} else if (this.props.showInvalidJoplinCloudCredential) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Your Joplin Cloud credentials are invalid, please login.'),
|
||||
_('Login to Joplin Cloud.'),
|
||||
onViewJoplinCloudLoginScreen,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -705,7 +719,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
props.isSafeMode ||
|
||||
this.showShareInvitationNotification(props) ||
|
||||
this.props.needApiAuth ||
|
||||
!!this.props.mustUpgradeAppMessage;
|
||||
!!this.props.mustUpgradeAppMessage ||
|
||||
props.showInvalidJoplinCloudCredential;
|
||||
}
|
||||
|
||||
public registerCommands() {
|
||||
@@ -965,6 +980,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
notesSortOrderField: state.settings['notes.sortOrder.field'],
|
||||
notesSortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ import shim from '@joplin/lib/shim';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { openItemById } from '../../NoteEditor/utils/contextMenu';
|
||||
const { parseResourceUrl, urlProtocol } = require('@joplin/lib/urlUtils');
|
||||
import { fileUrlToResourceUrl, parseResourceUrl, urlProtocol } from '@joplin/lib/urlUtils';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
const { urlDecode } = require('@joplin/lib/string-utils');
|
||||
import { urlDecode } from '@joplin/lib/string-utils';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openItem',
|
||||
@@ -16,6 +17,11 @@ export const runtime = (): CommandRuntime => {
|
||||
execute: async (context: CommandContext, link: string) => {
|
||||
if (!link) throw new Error('Link cannot be empty');
|
||||
|
||||
const fromFileUrl = fileUrlToResourceUrl(link, Setting.value('resourceDir'));
|
||||
if (fromFileUrl) {
|
||||
link = fromFileUrl;
|
||||
}
|
||||
|
||||
if (link.startsWith('joplin://') || link.startsWith(':/')) {
|
||||
const parsedUrl = parseResourceUrl(link);
|
||||
if (parsedUrl) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleNoteType',
|
||||
@@ -15,14 +14,7 @@ export const runtime = (): CommandRuntime => {
|
||||
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const note = await Note.load(noteIds[i]);
|
||||
const newNote = await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
||||
const eventNote = {
|
||||
id: newNote.id,
|
||||
is_todo: newNote.is_todo,
|
||||
todo_due: newNote.todo_due,
|
||||
todo_completed: newNote.todo_completed,
|
||||
};
|
||||
eventManager.emit(EventName.NoteTypeToggle, { noteId: note.id, note: eventNote });
|
||||
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
||||
}
|
||||
},
|
||||
enabledCondition: '!noteIsReadOnly',
|
||||
|
||||
@@ -4,7 +4,7 @@ import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
|
||||
import { EditorCommand } from '../../../../utils/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { EventName } from '@joplin/lib/eventManager';
|
||||
import normalizeAccelerator from '../../utils/normalizeAccelerator';
|
||||
import { CodeMirrorVersion } from '../../utils/types';
|
||||
|
||||
@@ -352,7 +352,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
return {
|
||||
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
readOnly: props.disabled || props.visiblePanes.indexOf('editor') < 0,
|
||||
readOnly: props.disabled,
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
themeData: {
|
||||
...styles.globalTheme,
|
||||
@@ -366,8 +366,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
indentWithTabs: true,
|
||||
};
|
||||
}, [
|
||||
props.contentMarkupLanguage, props.disabled, props.visiblePanes,
|
||||
props.keyboardMode, styles.globalTheme,
|
||||
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
|
||||
]);
|
||||
|
||||
// Update the editor's value
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
|
||||
@@ -12,25 +12,6 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
const logger = Logger.create('CodeMirror 6 commands');
|
||||
|
||||
const wrapSelectionWithStrings = (editor: CodeMirrorControl, string1: string, string2 = '', defaultText = '') => {
|
||||
if (editor.somethingSelected()) {
|
||||
editor.wrapSelections(string1, string2);
|
||||
} else {
|
||||
editor.wrapSelections(string1 + defaultText, string2);
|
||||
|
||||
// Now select the default text so the user can replace it
|
||||
const selections = editor.listSelections();
|
||||
const newSelections = [];
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
const s = selections[i];
|
||||
const anchor = { line: s.anchor.line, ch: s.anchor.ch + string1.length };
|
||||
const head = { line: s.head.line, ch: s.head.ch - string2.length };
|
||||
newSelections.push({ anchor: anchor, head: head });
|
||||
}
|
||||
editor.setSelections(newSelections);
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
webviewRef: RefObject<any>;
|
||||
@@ -92,7 +73,9 @@ const useEditorCommands = (props: Props) => {
|
||||
textLink: async () => {
|
||||
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||
focus('useEditorCommands::textLink', editorRef.current);
|
||||
if (url) wrapSelectionWithStrings(editorRef.current, '[', `](${url})`);
|
||||
if (url) {
|
||||
editorRef.current.wrapSelections('[', `](${url})`);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
insertText: (value: any) => editorRef.current.insertText(value),
|
||||
|
||||
@@ -32,6 +32,7 @@ import markupRenderOptions from '../../utils/markupRenderOptions';
|
||||
import { DropHandler } from '../../utils/useDropHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useWebViewApi from './utils/useWebViewApi';
|
||||
import useLinkTooltips from './utils/useLinkTooltips';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
const md5 = require('md5');
|
||||
const { clipboard } = require('electron');
|
||||
@@ -363,6 +364,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
}, []);
|
||||
|
||||
useWebViewApi(editor);
|
||||
const { resetModifiedTitles: resetLinkTooltips } = useLinkTooltips(editor);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
@@ -1048,6 +1050,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
|
||||
nextOnChangeEventInfo.current = null;
|
||||
|
||||
resetLinkTooltips();
|
||||
const contentMd = await prop_htmlToMarkdownRef.current(info.contentMarkupLanguage, info.editor.getContent(), info.contentOriginalCss);
|
||||
|
||||
lastOnChangeEventInfo.current.content = contentMd;
|
||||
|
||||
@@ -17,6 +17,8 @@ export const joplinCommandToTinyMceCommands: JoplinCommandToTinyMceCommands = {
|
||||
'textItalic': { name: 'mceToggleFormat', value: 'italic' },
|
||||
'textCode': { name: 'mceToggleFormat', value: 'code' },
|
||||
'textLink': { name: 'mceLink' },
|
||||
'textBulletedList': { name: 'InsertUnorderedList' },
|
||||
'textNumberedList': { name: 'InsertOrderedList' },
|
||||
'search': { name: 'SearchReplace' },
|
||||
'attachFile': { name: 'joplinAttach' },
|
||||
'insertDateTime': true,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Editor } from 'tinymce';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const useLinkTooltips = (editor: Editor|null) => {
|
||||
const resetModifiedTitles = useCallback(() => {
|
||||
for (const element of editor.getDoc().querySelectorAll('a[data-joplin-original-title]')) {
|
||||
element.setAttribute('title', element.getAttribute('data-joplin-original-title') ?? '');
|
||||
element.removeAttribute('data-joplin-original-title');
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const onMouseOver = (event: MouseEvent) => {
|
||||
let element = event.target as HTMLElement;
|
||||
|
||||
// mouseover events seem to only target the lowest applicable node in the DOM.
|
||||
// If the user's mouse enters <a><strong></strong></a>, the mouseover event will
|
||||
// target the <strong></strong>. As such, the parent nodes need to be checked:
|
||||
let counter = 0;
|
||||
while (element.tagName !== 'A' || !('href' in element)) {
|
||||
element = element.parentElement;
|
||||
counter++;
|
||||
if (!element || counter > 4) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!element.hasAttribute('data-joplin-original-title')) {
|
||||
element.setAttribute('data-joplin-original-title', element.title);
|
||||
}
|
||||
|
||||
// Avoid showing internal HREFs for note links.
|
||||
if (element.hasAttribute('data-resource-id') && !element.title) {
|
||||
if (shim.isMac()) {
|
||||
element.title = _('Cmd-click to open');
|
||||
} else {
|
||||
element.title = _('Ctrl-click to open');
|
||||
}
|
||||
} else {
|
||||
if (shim.isMac()) {
|
||||
element.title = _('Cmd-click to open: %s', element.title || element.href);
|
||||
} else {
|
||||
element.title = _('Ctrl-click to open: %s', element.title || element.href);
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
resetModifiedTitles();
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
};
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
};
|
||||
|
||||
const clearRootEventListeners = () => {
|
||||
editor.getDoc().removeEventListener('mouseover', onMouseOver);
|
||||
};
|
||||
|
||||
const setUpRootEventListeners = () => {
|
||||
clearRootEventListeners();
|
||||
editor.getDoc().addEventListener('mouseover', onMouseOver);
|
||||
};
|
||||
setUpRootEventListeners();
|
||||
editor.on('SetContent', setUpRootEventListeners);
|
||||
editor.on('keyup', resetModifiedTitles);
|
||||
editor.on('click', resetModifiedTitles);
|
||||
|
||||
return () => {
|
||||
resetModifiedTitles();
|
||||
editor.off('SetContent', setUpRootEventListeners);
|
||||
editor.off('keyup', resetModifiedTitles);
|
||||
editor.off('click', resetModifiedTitles);
|
||||
clearRootEventListeners();
|
||||
};
|
||||
}, [editor, resetModifiedTitles]);
|
||||
|
||||
return { resetModifiedTitles };
|
||||
};
|
||||
|
||||
export default useLinkTooltips;
|
||||
@@ -3,19 +3,18 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
import { htmlToMarkdown, formNoteToNote } from './utils';
|
||||
import { htmlToMarkdown } from './utils';
|
||||
import useSearchMarkers from './utils/useSearchMarkers';
|
||||
import useNoteSearchBar from './utils/useNoteSearchBar';
|
||||
import useMessageHandler from './utils/useMessageHandler';
|
||||
import useWindowCommandHandler from './utils/useWindowCommandHandler';
|
||||
import useDropHandler from './utils/useDropHandler';
|
||||
import useMarkupToHtml from './utils/useMarkupToHtml';
|
||||
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
|
||||
import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote';
|
||||
import useEffectiveNoteId from './utils/useEffectiveNoteId';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
@@ -26,7 +25,6 @@ import { _, _n } from '@joplin/lib/locale';
|
||||
import TagList from '../TagList';
|
||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||
import markupLanguageUtils from '../../utils/markupLanguageUtils';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
@@ -37,7 +35,7 @@ import NoteSearchBar from '../NoteSearchBar';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../../services/bridge';
|
||||
import NoteRevisionViewer from '../NoteRevisionViewer';
|
||||
import { parseShareCache } from '@joplin/lib/services/share/reducer';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
@@ -51,6 +49,9 @@ import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
|
||||
import { openItemById } from './utils/contextMenu';
|
||||
import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
||||
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
||||
const debounce = require('debounce');
|
||||
|
||||
const commands = [
|
||||
require('./commands/showRevisions'),
|
||||
@@ -61,20 +62,21 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
function NoteEditor(props: NoteEditorProps) {
|
||||
const [showRevisions, setShowRevisions] = useState(false);
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions>(null);
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
|
||||
|
||||
const editorRef = useRef<NoteBodyEditorRef>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const titleInputRef = useRef<any>();
|
||||
const titleInputRef = useRef<HTMLInputElement>();
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
|
||||
const setFormNoteRef = useRef<OnSetFormNote>();
|
||||
const { saveNoteIfWillChange, scheduleSaveNote } = useScheduleSaveCallbacks({
|
||||
setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef,
|
||||
});
|
||||
const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => {
|
||||
await saveNoteIfWillChange(event.formNote);
|
||||
setShowRevisions(false);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}, [saveNoteIfWillChange]);
|
||||
|
||||
const formNote_afterLoad = useCallback(async () => {
|
||||
setTitleHasBeenManuallyChanged(false);
|
||||
@@ -92,7 +94,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
onBeforeLoad: formNote_beforeLoad,
|
||||
onAfterLoad: formNote_afterLoad,
|
||||
});
|
||||
|
||||
setFormNoteRef.current = setFormNote;
|
||||
const formNoteRef = useRef<FormNote>();
|
||||
formNoteRef.current = { ...formNote };
|
||||
|
||||
@@ -116,53 +118,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
const styles = styles_(props);
|
||||
|
||||
function scheduleSaveNote(formNote: FormNote) {
|
||||
if (!formNote.saveActionQueue) throw new Error('saveActionQueue is not set!!'); // Sanity check
|
||||
|
||||
// reg.logger().debug('Scheduling...', formNote);
|
||||
|
||||
const makeAction = (formNote: FormNote) => {
|
||||
return async function() {
|
||||
const note = await formNoteToNote(formNote);
|
||||
reg.logger().debug('Saving note...', note);
|
||||
const savedNote = await Note.save(note);
|
||||
|
||||
setFormNote((prev: FormNote) => {
|
||||
return { ...prev, user_updated_time: savedNote.user_updated_time, hasChanged: false };
|
||||
});
|
||||
|
||||
void ExternalEditWatcher.instance().updateNoteFile(savedNote);
|
||||
|
||||
props.dispatch({
|
||||
type: 'EDITOR_NOTE_STATUS_REMOVE',
|
||||
id: formNote.id,
|
||||
});
|
||||
|
||||
eventManager.emit(EventName.NoteContentChange, { note: savedNote });
|
||||
};
|
||||
};
|
||||
|
||||
formNote.saveActionQueue.push(makeAction(formNote));
|
||||
}
|
||||
|
||||
async function saveNoteIfWillChange(formNote: FormNote) {
|
||||
if (!formNote.id || !formNote.bodyWillChangeId) return;
|
||||
|
||||
const body = await editorRef.current.content();
|
||||
|
||||
scheduleSaveNote({
|
||||
...formNote,
|
||||
body: body,
|
||||
bodyWillChangeId: 0,
|
||||
bodyChangeId: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function saveNoteAndWait(formNote: FormNote) {
|
||||
await saveNoteIfWillChange(formNote);
|
||||
return formNote.saveActionQueue.waitForAllDone();
|
||||
}
|
||||
|
||||
const whiteBackgroundNoteRendering = formNote.markup_language === MarkupLanguage.Html;
|
||||
|
||||
const markupToHtml = useMarkupToHtml({
|
||||
@@ -201,29 +156,18 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
id: formNote.id,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.isProvisional, formNote.id]);
|
||||
}, [props.isProvisional, formNote.id, props.dispatch]);
|
||||
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (formNote.id === previousNoteId) return;
|
||||
|
||||
if (editorRef.current) {
|
||||
editorRef.current.resetScroll();
|
||||
}
|
||||
|
||||
setScrollWhenReady({
|
||||
type: props.selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||
value: props.selectedNoteHash ? props.selectedNoteHash : props.lastEditorScrollPercents[formNote.id] || 0,
|
||||
});
|
||||
|
||||
void ResourceEditWatcher.instance().stopWatchingAll();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote.id, previousNoteId]);
|
||||
const scheduleNoteListResort = useMemo(() => {
|
||||
return debounce(() => {
|
||||
// Although the note list will update automatically, it may take some time. This
|
||||
// forces an immediate update.
|
||||
props.dispatch({ type: 'NOTE_SORT' });
|
||||
}, 100);
|
||||
}, [props.dispatch]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onFieldChange = useCallback((field: string, value: any, changeId = 0) => {
|
||||
const onFieldChange = useCallback(async (field: string, value: any, changeId = 0) => {
|
||||
if (!isMountedRef.current) {
|
||||
// When the component is unmounted, various actions can happen which can
|
||||
// trigger onChange events, for example the textarea might be cleared.
|
||||
@@ -263,19 +207,23 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
// The previously loaded note, that was modified, will be saved via saveNoteIfWillChange()
|
||||
} else {
|
||||
setFormNote(newNote);
|
||||
scheduleSaveNote(newNote);
|
||||
await scheduleSaveNote(newNote);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [handleProvisionalFlag, formNote, isNewNote, titleHasBeenManuallyChanged]);
|
||||
|
||||
if (field === 'title') {
|
||||
// Scheduling a resort needs to be:
|
||||
// - called after scheduleSaveNote so that the new note title is used for sorting
|
||||
// - debounced because many calls to scheduleSaveNote can resolve at once
|
||||
scheduleNoteListResort();
|
||||
}
|
||||
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
|
||||
|
||||
useWindowCommandHandler({
|
||||
dispatch: props.dispatch,
|
||||
formNote,
|
||||
setShowLocalSearch,
|
||||
noteSearchBarRef,
|
||||
editorRef,
|
||||
titleInputRef,
|
||||
saveNoteAndWait,
|
||||
setFormNote,
|
||||
});
|
||||
|
||||
@@ -339,10 +287,15 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
id: formNote.id,
|
||||
status: 'saving',
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote, handleProvisionalFlag]);
|
||||
}, [formNote, setFormNote, handleProvisionalFlag, props.dispatch]);
|
||||
|
||||
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
const { scrollWhenReady, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||
noteId: formNote.id,
|
||||
selectedNoteHash: props.selectedNoteHash,
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
});
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
||||
@@ -355,8 +308,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
setFormNote(newFormNote);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote]);
|
||||
}, [formNote, setFormNote]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onNotePropertyChange = useCallback((event: any) => {
|
||||
@@ -373,8 +325,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
return newFormNote;
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}, [setFormNote]);
|
||||
|
||||
useEffect(() => {
|
||||
eventManager.on(EventName.AlarmChange, onNotePropertyChange);
|
||||
@@ -409,8 +360,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function renderNoNotes(rootStyle: any) {
|
||||
function renderNoNotes(rootStyle: React.CSSProperties) {
|
||||
const emptyDivStyle = {
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.1,
|
||||
@@ -493,7 +443,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}
|
||||
|
||||
const onRichTextReadMoreLinkClick = useCallback(() => {
|
||||
bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
|
||||
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
|
||||
}, []);
|
||||
|
||||
const onRichTextDismissLinkClick = useCallback(() => {
|
||||
@@ -521,8 +471,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
if (showRevisions) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const revStyle: any = {
|
||||
const revStyle: React.CSSProperties = {
|
||||
// ...props.style,
|
||||
display: 'inline-flex',
|
||||
padding: theme.margin,
|
||||
|
||||
@@ -36,8 +36,7 @@ interface Props {
|
||||
noteTitle: string;
|
||||
noteIsTodo: number;
|
||||
isProvisional: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
titleInputRef: any;
|
||||
titleInputRef: React.RefObject<HTMLInputElement>;
|
||||
onTitleChange(event: ChangeEvent<HTMLInputElement>): void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import htmlUtils from '@joplin/lib/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody, removeWrappingParagraphAndTrailingEmptyElements } from '@joplin/renderer/htmlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
@@ -15,7 +15,7 @@ import markupRenderOptions from './markupRenderOptions';
|
||||
import { fileExtension, filename, safeFileExtension, safeFilename } from '@joplin/utils/path';
|
||||
const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
const { clipboard } = require('electron');
|
||||
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
const md5 = require('md5');
|
||||
const path = require('path');
|
||||
|
||||
@@ -220,6 +220,13 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
|
||||
if (htmlToMd && mdToHtml) {
|
||||
const md = await htmlToMd(MarkupLanguage.Markdown, html, '');
|
||||
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
|
||||
|
||||
// When plugins that add to the end of rendered content are installed, bodyOnly can
|
||||
// fail to remove the wrapping paragraph. This works around that issue by removing
|
||||
// the wrapping paragraph in more cases. See issue #10061.
|
||||
if (!md.trim().includes('\n')) {
|
||||
html = removeWrappingParagraphAndTrailingEmptyElements(html);
|
||||
}
|
||||
}
|
||||
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
const defaultFormNoteProps: HookDependencies = {
|
||||
syncStarted: false,
|
||||
@@ -75,6 +77,8 @@ describe('useFormNote', () => {
|
||||
title: 'Test Note!',
|
||||
});
|
||||
});
|
||||
|
||||
formNote.unmount();
|
||||
});
|
||||
|
||||
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
|
||||
@@ -109,4 +113,43 @@ describe('useFormNote', () => {
|
||||
// });
|
||||
// });
|
||||
|
||||
test('should refresh resource infos when changed outside the editor', async () => {
|
||||
let note = await Note.save({});
|
||||
note = await shim.attachFileToNote(note, __filename);
|
||||
const resourceIds = Note.linkedItemIds(note.body);
|
||||
const resource = await Resource.load(resourceIds[0]);
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
return {
|
||||
...defaultFormNoteProps,
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: note.id,
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
});
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
return Object.values(formNote.result.current.resourceInfos).length > 0;
|
||||
});
|
||||
const initialResourceInfos = formNote.result.current.resourceInfos;
|
||||
expect(initialResourceInfos).toMatchObject({
|
||||
[resource.id]: { item: { id: resource.id } },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Resource.save({ ...resource, filename: 'test.ts' });
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
const resourceInfo = formNote.result.current.resourceInfos[resource.id];
|
||||
expect(resourceInfo.item).toMatchObject({
|
||||
id: resource.id, filename: 'test.ts',
|
||||
});
|
||||
});
|
||||
|
||||
formNote.unmount();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, RefObject, useRef } from 'react';
|
||||
import { FormNote, defaultFormNote, ResourceInfos } from './types';
|
||||
import { clearResourceCache, attachedResources } from './resourceHandling';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
@@ -6,15 +6,17 @@ import { handleResourceDownloadMode } from './resourceHandling';
|
||||
import { splitHtml } from '@joplin/renderer/HtmlToHtml';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
|
||||
const logger = Logger.create('useFormNote');
|
||||
|
||||
export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
@@ -25,28 +27,28 @@ export interface HookDependencies {
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
isProvisional: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
titleInputRef: any;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editorRef: any;
|
||||
onBeforeLoad(event: OnLoadEvent): void;
|
||||
onAfterLoad(event: OnLoadEvent): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function installResourceChangeHandler(onResourceChangeHandler: Function) {
|
||||
type MapFormNoteCallback = (previousFormNote: FormNote)=> FormNote;
|
||||
export type OnSetFormNote = (newFormNote: FormNote|MapFormNoteCallback)=> void;
|
||||
|
||||
function installResourceChangeHandler(onResourceChangeHandler: ()=> void) {
|
||||
ResourceFetcher.instance().on('downloadComplete', onResourceChangeHandler);
|
||||
ResourceFetcher.instance().on('downloadStarted', onResourceChangeHandler);
|
||||
DecryptionWorker.instance().on('resourceDecrypted', onResourceChangeHandler);
|
||||
ResourceEditWatcher.instance().on('resourceChange', onResourceChangeHandler);
|
||||
eventManager.on(EventName.ResourceChange, onResourceChangeHandler);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function uninstallResourceChangeHandler(onResourceChangeHandler: Function) {
|
||||
function uninstallResourceChangeHandler(onResourceChangeHandler: ()=> void) {
|
||||
ResourceFetcher.instance().off('downloadComplete', onResourceChangeHandler);
|
||||
ResourceFetcher.instance().off('downloadStarted', onResourceChangeHandler);
|
||||
DecryptionWorker.instance().off('resourceDecrypted', onResourceChangeHandler);
|
||||
ResourceEditWatcher.instance().off('resourceChange', onResourceChangeHandler);
|
||||
eventManager.off(EventName.ResourceChange, onResourceChangeHandler);
|
||||
}
|
||||
|
||||
function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
@@ -77,11 +79,14 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
const formNoteRef = useRef(formNote);
|
||||
formNoteRef.current = formNote;
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: NoteEntity) {
|
||||
const initNoteState = useCallback(async (n: NoteEntity, isNewNote: boolean) => {
|
||||
let originalCss = '';
|
||||
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
@@ -106,22 +111,39 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
encryption_applied: n.encryption_applied,
|
||||
};
|
||||
|
||||
logger.debug('Initializing note state');
|
||||
|
||||
// Note that for performance reason,the call to setResourceInfos should
|
||||
// be first because it loads the resource infos in an async way. If we
|
||||
// swap them, the formNote will be updated first and rendered, then the
|
||||
// the resources will load, and the note will be re-rendered.
|
||||
setResourceInfos(await attachedResources(n.body));
|
||||
const resources = await attachedResources(n.body);
|
||||
|
||||
// If the user changes the note while resources are loading, this can lead to
|
||||
// a note being incorrectly marked as "unchanged".
|
||||
if (!isNewNote && formNoteRef.current?.hasChanged) {
|
||||
logger.info('Cancelled note refresh -- form note changed while loading attached resources.');
|
||||
return null;
|
||||
}
|
||||
|
||||
setResourceInfos(resources);
|
||||
setFormNote(newFormNote);
|
||||
|
||||
logger.debug('Resource info and form note set.');
|
||||
|
||||
await handleResourceDownloadMode(n.body);
|
||||
|
||||
return newFormNote;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formNoteRefreshScheduled <= 0) return () => {};
|
||||
if (formNoteRef.current.hasChanged) {
|
||||
logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
@@ -133,11 +155,12 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
reg.logger().warn('Trying to reload note that has been deleted:', noteId);
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await initNoteState(n);
|
||||
await initNoteState(n, false);
|
||||
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
@@ -146,7 +169,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [formNoteRefreshScheduled, noteId]);
|
||||
}, [formNoteRefreshScheduled, noteId, initNoteState]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
@@ -163,7 +186,9 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNote.hasChanged) return;
|
||||
if (formNoteRef.current.hasChanged) return;
|
||||
|
||||
logger.debug('Sync or decryption finished with an unchanged formNote.');
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
@@ -172,7 +197,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
formNote.hasChanged, refreshFormNote,
|
||||
refreshFormNote,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -185,7 +210,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
reg.logger().debug('Loading existing note', noteId);
|
||||
logger.debug('Loading existing note', noteId);
|
||||
|
||||
function handleAutoFocus(noteIsTodo: boolean) {
|
||||
if (!isProvisional) return;
|
||||
@@ -205,11 +230,11 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
const n = await Note.load(noteId);
|
||||
if (cancelled) return;
|
||||
if (!n) throw new Error(`Cannot find note with ID: ${noteId}`);
|
||||
reg.logger().debug('Loaded note:', n);
|
||||
logger.debug('Loaded note:', n);
|
||||
|
||||
await onBeforeLoad({ formNote });
|
||||
|
||||
const newFormNote = await initNoteState(n);
|
||||
const newFormNote = await initNoteState(n, true);
|
||||
|
||||
setIsNewNote(isProvisional);
|
||||
|
||||
@@ -231,7 +256,8 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
const resourceIds = await Note.linkedResourceIds(formNote.body);
|
||||
if (!event || resourceIds.indexOf(event.id) >= 0) {
|
||||
clearResourceCache();
|
||||
setResourceInfos(await attachedResources(formNote.body));
|
||||
const newResourceInfos = await attachedResources(formNote.body);
|
||||
setResourceInfos(newResourceInfos);
|
||||
}
|
||||
}, [formNote.body]);
|
||||
|
||||
@@ -266,5 +292,24 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
};
|
||||
}, [formNote.body]);
|
||||
|
||||
return { isNewNote, formNote, setFormNote, resourceInfos };
|
||||
// Currently, useFormNote relies on formNoteRef being up-to-date immediately after the editor
|
||||
// changes, with no delay during which async code can run. Even a small delay (e.g. that introduced
|
||||
// by a setState -> useEffect) can lead to a race condition. See https://github.com/laurent22/joplin/issues/8960.
|
||||
const onSetFormNote: OnSetFormNote = useCallback(newFormNote => {
|
||||
if (typeof newFormNote === 'function') {
|
||||
const newNote = newFormNote(formNoteRef.current);
|
||||
formNoteRef.current = newNote;
|
||||
setFormNote(newNote);
|
||||
} else {
|
||||
formNoteRef.current = newFormNote;
|
||||
setFormNote(newFormNote);
|
||||
}
|
||||
}, [setFormNote]);
|
||||
|
||||
return {
|
||||
isNewNote,
|
||||
formNote,
|
||||
setFormNote: onSetFormNote,
|
||||
resourceInfos,
|
||||
};
|
||||
}
|
||||
@@ -25,7 +25,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
customCss: customCss || '',
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [plugins, customCss]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -63,6 +62,5 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
});
|
||||
|
||||
return result;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [themeId, customCss, markupToHtml, whiteBackgroundNoteRendering]);
|
||||
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, deps.settingValue]);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler, ScrollOptions } from './types';
|
||||
import contextMenu from './contextMenu';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||
@@ -8,7 +8,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
export default function useMessageHandler(scrollWhenReady: any, setScrollWhenReady: Function, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||
export default function useMessageHandler(scrollWhenReady: ScrollOptions|null, clearScrollWhenReady: ()=> void, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return useCallback(async (event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
@@ -25,7 +25,7 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
} else if (msg === 'noteRenderComplete') {
|
||||
if (scrollWhenReady) {
|
||||
const options = { ...scrollWhenReady };
|
||||
setScrollWhenReady(null);
|
||||
clearScrollWhenReady();
|
||||
editorRef.current.scrollTo(options);
|
||||
}
|
||||
} else if (msg === 'setMarkerCount') {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { FormNote, NoteBodyEditorRef } from './types';
|
||||
import { formNoteToNote } from '.';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import type { Dispatch } from 'redux';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import type { OnSetFormNote } from './useFormNote';
|
||||
|
||||
const logger = Logger.create('useScheduleSaveCallbacks');
|
||||
|
||||
interface Props {
|
||||
setFormNote: RefObject<OnSetFormNote>;
|
||||
dispatch: Dispatch;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
|
||||
const useScheduleSaveCallbacks = (props: Props) => {
|
||||
const scheduleSaveNote = useCallback((formNote: FormNote) => {
|
||||
if (!formNote.saveActionQueue) throw new Error('saveActionQueue is not set!!'); // Sanity check
|
||||
|
||||
// reg.logger().debug('Scheduling...', formNote);
|
||||
|
||||
const makeAction = (formNote: FormNote) => {
|
||||
return async function() {
|
||||
const note = await formNoteToNote(formNote);
|
||||
logger.debug('Saving note...', note);
|
||||
const savedNote = await Note.save(note);
|
||||
|
||||
props.setFormNote.current((prev: FormNote) => {
|
||||
return { ...prev, user_updated_time: savedNote.user_updated_time, hasChanged: false };
|
||||
});
|
||||
|
||||
void ExternalEditWatcher.instance().updateNoteFile(savedNote);
|
||||
|
||||
props.dispatch({
|
||||
type: 'EDITOR_NOTE_STATUS_REMOVE',
|
||||
id: formNote.id,
|
||||
});
|
||||
|
||||
eventManager.emit(EventName.NoteContentChange, { note: savedNote });
|
||||
};
|
||||
};
|
||||
|
||||
formNote.saveActionQueue.push(makeAction(formNote));
|
||||
return formNote.saveActionQueue.waitForAllDone();
|
||||
}, [props.dispatch, props.setFormNote]);
|
||||
|
||||
const saveNoteIfWillChange = useCallback(async (formNote: FormNote) => {
|
||||
if (!formNote.id || !formNote.bodyWillChangeId) return;
|
||||
|
||||
const body = await props.editorRef.current.content();
|
||||
|
||||
void scheduleSaveNote({
|
||||
...formNote,
|
||||
body: body,
|
||||
bodyWillChangeId: 0,
|
||||
bodyChangeId: 0,
|
||||
});
|
||||
}, [scheduleSaveNote, props.editorRef]);
|
||||
|
||||
return { saveNoteIfWillChange, scheduleSaveNote };
|
||||
};
|
||||
|
||||
export default useScheduleSaveCallbacks;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
|
||||
import type { EditorScrollPercents } from '../../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
selectedNoteHash: string;
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
|
||||
const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollPercents, editorRef }: Props) => {
|
||||
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions|null>(null);
|
||||
|
||||
const previousNoteId = usePrevious(noteId);
|
||||
const lastScrollPercentsRef = useRef<EditorScrollPercents>();
|
||||
lastScrollPercentsRef.current = lastEditorScrollPercents;
|
||||
|
||||
useEffect(() => {
|
||||
if (noteId === previousNoteId) return;
|
||||
|
||||
if (editorRef.current) {
|
||||
editorRef.current.resetScroll();
|
||||
}
|
||||
|
||||
const lastScrollPercent = lastScrollPercentsRef.current[noteId] || 0;
|
||||
setScrollWhenReady({
|
||||
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
||||
});
|
||||
|
||||
void ResourceEditWatcher.instance().stopWatchingAll();
|
||||
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
||||
|
||||
const clearScrollWhenReady = useCallback(() => {
|
||||
setScrollWhenReady(null);
|
||||
}, []);
|
||||
|
||||
return { scrollWhenReady, clearScrollWhenReady };
|
||||
};
|
||||
|
||||
export default useScrollWhenReadyOptions;
|
||||
@@ -15,7 +15,6 @@ const commandsWithDependencies = [
|
||||
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
|
||||
|
||||
interface HookDependencies {
|
||||
formNote: FormNote;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
setShowLocalSearch: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@@ -23,10 +22,7 @@ interface HookDependencies {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
noteSearchBarRef: any;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
titleInputRef: any;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
saveNoteAndWait: Function;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
setFormNote: SetFormNoteCallback;
|
||||
}
|
||||
|
||||
@@ -109,6 +105,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef]);
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
|
||||
}
|
||||
@@ -65,7 +65,7 @@ const NoteList = (props: Props) => {
|
||||
props.notes.length,
|
||||
);
|
||||
|
||||
const focusNote = useFocusNote(itemRefs);
|
||||
const focusNote = useFocusNote(itemRefs, props.notes, makeItemIndexVisible);
|
||||
|
||||
const moveNote = useMoveNote(
|
||||
props.notesParentType,
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { useRef, useCallback, MutableRefObject } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
export type FocusNote = (noteId: string)=> void;
|
||||
type ItemRefs = MutableRefObject<Record<string, HTMLDivElement>>;
|
||||
type OnMakeIndexVisible = (i: number)=> void;
|
||||
|
||||
const useFocusNote = (itemRefs: MutableRefObject<Record<string, HTMLDivElement>>) => {
|
||||
const useFocusNote = (itemRefs: ItemRefs, notes: NoteEntity[], makeItemIndexVisible: OnMakeIndexVisible) => {
|
||||
const focusItemIID = useRef(null);
|
||||
|
||||
const notesRef = useRef(notes);
|
||||
notesRef.current = notes;
|
||||
|
||||
const focusNote: FocusNote = useCallback((noteId: string) => {
|
||||
// - We need to focus the item manually otherwise focus might be lost when the
|
||||
// list is scrolled and items within it are being rebuilt.
|
||||
// - We need to use an interval because when leaving the arrow pressed, the rendering
|
||||
// of items might lag behind and so the ref is not yet available at this point.
|
||||
// - We need to use an interval because when leaving the arrow pressed or scrolling
|
||||
// offscreen items into view, the rendering of items might lag behind and so the
|
||||
// ref is not yet available at this point.
|
||||
|
||||
if (!itemRefs.current[noteId]) {
|
||||
const targetIndex = notesRef.current.findIndex(note => note.id === noteId);
|
||||
if (targetIndex > -1) {
|
||||
makeItemIndexVisible(targetIndex);
|
||||
}
|
||||
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
focusItemIID.current = shim.setInterval(() => {
|
||||
if (itemRefs.current[noteId]) {
|
||||
@@ -26,7 +38,7 @@ const useFocusNote = (itemRefs: MutableRefObject<Record<string, HTMLDivElement>>
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
focus('useFocusNote2', itemRefs.current[noteId]);
|
||||
}
|
||||
}, [itemRefs]);
|
||||
}, [itemRefs, makeItemIndexVisible]);
|
||||
|
||||
return focusNote;
|
||||
};
|
||||
|
||||
@@ -104,10 +104,17 @@ const useOnKeyDown = (
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
if (noteIds.length) {
|
||||
if (key === 'Delete' && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
|
||||
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
|
||||
}
|
||||
} else if (key === 'Delete' || (key === 'Backspace' && event.metaKey)) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const { themeStyle } = require('@joplin/lib/theme');
|
||||
import bridge from '../services/bridge';
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { LoadOptions } from '@joplin/lib/models/utils/types';
|
||||
|
||||
interface Style {
|
||||
width: number;
|
||||
@@ -31,6 +32,7 @@ interface State {
|
||||
resources: InnerResource[] | undefined;
|
||||
sorting: ActiveSorting;
|
||||
isLoading: boolean;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
interface ResourceTable {
|
||||
@@ -42,6 +44,7 @@ interface ResourceTable {
|
||||
onResourceDelete: (resource: InnerResource)=> any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onToggleSorting: (order: SortingOrder)=> any;
|
||||
filter: string;
|
||||
themeId: number;
|
||||
style: Style;
|
||||
}
|
||||
@@ -89,6 +92,10 @@ const ResourceTableComp = (props: ResourceTable) => {
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
const filteredResources = props.resources.filter(
|
||||
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
|
||||
);
|
||||
|
||||
return (
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
@@ -100,7 +107,7 @@ const ResourceTableComp = (props: ResourceTable) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.resources.map((resource: InnerResource, index: number) =>
|
||||
{filteredResources.map((resource: InnerResource, index: number) =>
|
||||
<tr key={index}>
|
||||
<td style={titleCellStyle} className="titleCell">
|
||||
<a
|
||||
@@ -136,13 +143,15 @@ const getNextSortingOrderType = (s: SortingType): SortingType => {
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_RESOURCES = 10000;
|
||||
const defaultMaxResources = 10000;
|
||||
const searchMaxResources = 1000;
|
||||
|
||||
class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
resources: undefined,
|
||||
filter: '',
|
||||
sorting: {
|
||||
type: 'asc',
|
||||
order: 'name',
|
||||
@@ -151,22 +160,57 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
public async reloadResources(sorting: ActiveSorting) {
|
||||
private get maxResources() {
|
||||
// Use a smaller maximum when searching for performance -- results update
|
||||
// when the search input changes.
|
||||
if (this.state.filter) {
|
||||
return searchMaxResources;
|
||||
} else {
|
||||
return defaultMaxResources;
|
||||
}
|
||||
}
|
||||
|
||||
private reloadResourcesCounter = 0;
|
||||
public async reloadResources() {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
this.reloadResourcesCounter ++;
|
||||
const currentCounterValue = this.reloadResourcesCounter;
|
||||
|
||||
let searchOptions: Partial<LoadOptions> = {};
|
||||
if (this.state.filter) {
|
||||
const search = `%${this.state.filter}%`;
|
||||
searchOptions = {
|
||||
where: 'id LIKE ? OR title LIKE ?',
|
||||
whereParams: [search, search],
|
||||
};
|
||||
}
|
||||
|
||||
const resources = await Resource.all({
|
||||
order: [{
|
||||
by: getSortingOrderColumn(sorting.order),
|
||||
dir: sorting.type,
|
||||
by: getSortingOrderColumn(this.state.sorting.order),
|
||||
dir: this.state.sorting.type,
|
||||
caseInsensitive: true,
|
||||
}],
|
||||
limit: MAX_RESOURCES,
|
||||
limit: this.maxResources,
|
||||
fields: ['title', 'id', 'size', 'file_extension'],
|
||||
...searchOptions,
|
||||
});
|
||||
|
||||
const cancelled = currentCounterValue !== this.reloadResourcesCounter;
|
||||
if (cancelled) return;
|
||||
|
||||
this.setState({ resources, isLoading: false });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
void this.reloadResources(this.state.sorting);
|
||||
void this.reloadResources();
|
||||
}
|
||||
|
||||
public componentDidUpdate(_prevProps: Props, prevState: State) {
|
||||
if (prevState.sorting !== this.state.sorting || prevState.filter !== this.state.filter) {
|
||||
void this.reloadResources();
|
||||
}
|
||||
}
|
||||
|
||||
public onResourceDelete(resource: InnerResource) {
|
||||
@@ -185,7 +229,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.finally(() => {
|
||||
void this.reloadResources(this.state.sorting);
|
||||
void this.reloadResources();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,9 +252,12 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
this.setState({ sorting: newSorting });
|
||||
void this.reloadResources(newSorting);
|
||||
}
|
||||
|
||||
public onFilterUpdate = (updateEvent: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ filter: updateEvent.target.value });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
@@ -236,20 +283,30 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
|
||||
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
|
||||
}</div>
|
||||
<div style={{ float: 'right' }}>
|
||||
<input
|
||||
style={theme.inputStyle}
|
||||
type="search"
|
||||
value={this.state.filter}
|
||||
onChange={this.onFilterUpdate}
|
||||
placeholder={_('Search...')}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isLoading && <div>{_('Please wait...')}</div>}
|
||||
{!this.state.isLoading && <div>
|
||||
{!this.state.resources && <div>
|
||||
{_('No resources!')}
|
||||
</div>
|
||||
}
|
||||
{this.state.resources && this.state.resources.length === MAX_RESOURCES &&
|
||||
<div>{_('Warning: not all resources shown for performance reasons (limit: %s).', MAX_RESOURCES)}</div>
|
||||
{this.state.resources && this.state.resources.length === this.maxResources &&
|
||||
<div>{_('Warning: not all resources shown for performance reasons (limit: %s).', this.maxResources)}</div>
|
||||
}
|
||||
{this.state.resources && <ResourceTableComp
|
||||
themeId={this.props.themeId}
|
||||
style={style}
|
||||
resources={this.state.resources}
|
||||
sorting={this.state.sorting}
|
||||
filter={this.state.filter}
|
||||
onToggleSorting={(order) => this.onToggleSortOrder(order)}
|
||||
onResourceClick={(resource) => this.openResource(resource)}
|
||||
onResourceDelete={(resource) => this.onResourceDelete(resource)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagListItem } from '../types';
|
||||
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -35,10 +35,13 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
});
|
||||
}, [props.tags]);
|
||||
|
||||
const folderTree = useMemo(() => {
|
||||
return buildFolderTree(props.folders);
|
||||
}, [props.folders]);
|
||||
|
||||
const folderItems = useMemo(() => {
|
||||
const renderProps = {
|
||||
folders: props.folders,
|
||||
folderTree,
|
||||
collapsedFolderIds: props.collapsedFolderIds,
|
||||
};
|
||||
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
|
||||
@@ -50,7 +53,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
key: folder.id,
|
||||
};
|
||||
});
|
||||
}, [props.folders, props.collapsedFolderIds]);
|
||||
}, [folderTree, props.collapsedFolderIds]);
|
||||
|
||||
return useMemo(() => {
|
||||
const foldersHeader: HeaderListItem = {
|
||||
|
||||
@@ -84,6 +84,11 @@ export const StyledListItemAnchor = styled.a`
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
|
||||
/* A different background color is already used to indicate focus for sidebar list items. */
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledShareIcon = styled.i`
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
|
||||
if (!processedAssetIds.includes(assetId)) {
|
||||
try {
|
||||
asset.element.remove();
|
||||
if (asset?.element) asset.element.remove();
|
||||
} catch (error) {
|
||||
// We don't throw an exception but we log it since
|
||||
// it shouldn't happen
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { utils as pluginUtils, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
@@ -78,7 +77,6 @@ export default class NoteListUtils {
|
||||
const newNote = Note.changeNoteType(note, type);
|
||||
if (newNote === note) continue;
|
||||
await Note.save(newNote, { userSideValidation: true });
|
||||
eventManager.emit(EventName.NoteTypeToggle, { noteId: note.id });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -89,50 +89,6 @@ test.describe('main', () => {
|
||||
await expect(viewerFrame.locator('.joplin-editable > .katex').first()).toBeAttached();
|
||||
});
|
||||
|
||||
test('HTML links should be preserved when editing a note in the WYSIWYG editor', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('Testing!');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
// Set the note's content
|
||||
await editor.focusCodeMirrorEditor();
|
||||
|
||||
// Attach this file to the note (create a resource ID)
|
||||
await setFilePickerResponse(electronApp, [__filename]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
// Wait to render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await viewerFrame.locator('a[data-from-md]').waitFor();
|
||||
|
||||
// Should have an attached resource
|
||||
const codeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
|
||||
const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/;
|
||||
expect(codeMirrorContent).toMatch(resourceUrlExpression);
|
||||
const resourceId = codeMirrorContent.match(resourceUrlExpression)[1];
|
||||
|
||||
// Create a new note with just an HTML link
|
||||
await mainScreen.createNewNote('Another test');
|
||||
await editor.codeMirrorEditor.click();
|
||||
await mainWindow.keyboard.type(`<a href=":/${resourceId}">HTML Link</a>`);
|
||||
|
||||
// Switch to the RTE
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.richTextEditor.waitFor();
|
||||
|
||||
// Edit the note to cause the original content to update
|
||||
await editor.getTinyMCEFrameLocator().locator('a').click();
|
||||
await mainWindow.keyboard.type('Test...');
|
||||
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.codeMirrorEditor.waitFor();
|
||||
|
||||
// Note should still contain the resource ID and note title
|
||||
const finalCodeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
expect(finalCodeMirrorContent).toContain(`:/${resourceId}`);
|
||||
});
|
||||
|
||||
test('should correctly resize large images', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('Image resize test (part 1)');
|
||||
|
||||
@@ -48,4 +48,9 @@ export default class MainScreen {
|
||||
throw new Error('Unable to find settings menu item in application menus.');
|
||||
}
|
||||
}
|
||||
|
||||
public async search(text: string) {
|
||||
const searchBar = this.page.getByPlaceholder('Search...');
|
||||
await searchBar.fill(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
|
||||
test.describe('noteList', () => {
|
||||
test('should be possible to edit notes in a different notebook when searching', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
const folderAHeader = await sidebar.createNewFolder('Folder A');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder B');
|
||||
await expect(folderBHeader).toBeVisible();
|
||||
await folderBHeader.click();
|
||||
|
||||
await mainScreen.createNewNote('note-1');
|
||||
|
||||
await folderAHeader.click();
|
||||
await mainScreen.createNewNote('note-2');
|
||||
|
||||
// Search for and focus a note different from the folder we were in before searching.
|
||||
await mainScreen.search('/note-1');
|
||||
const note1Result = mainScreen.noteListContainer.getByText('note-1');
|
||||
await expect(note1Result).toBeAttached();
|
||||
await note1Result.click();
|
||||
|
||||
// Typing should not cause the note to disappear
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.codeMirrorEditor.click();
|
||||
await mainWindow.keyboard.type('[Testing...](http://example.com/)');
|
||||
|
||||
// Wait to render
|
||||
await expect(editor.getNoteViewerIframe().locator('a', { hasText: 'Testing...' })).toBeVisible();
|
||||
|
||||
// Updating the title should force the sidebar to update sooner
|
||||
await expect(editor.noteTitleInput).toHaveValue('note-1');
|
||||
});
|
||||
|
||||
test('shift-delete should ask to permanently delete notes, but only when the note list is focused', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder B');
|
||||
const folderAHeader = await sidebar.createNewFolder('Folder A');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
|
||||
await mainScreen.createNewNote('test note 1');
|
||||
await mainScreen.createNewNote('test note 2');
|
||||
|
||||
await activateMainMenuItem(electronApp, 'Note list', 'Focus');
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible();
|
||||
|
||||
await setMessageBoxResponse(electronApp, /^Delete/i);
|
||||
|
||||
const pressShiftDelete = async () => {
|
||||
await mainWindow.keyboard.press('Shift');
|
||||
await mainWindow.keyboard.press('Delete');
|
||||
await mainWindow.keyboard.up('Delete');
|
||||
await mainWindow.keyboard.up('Shift');
|
||||
};
|
||||
await pressShiftDelete();
|
||||
|
||||
await folderBHeader.click();
|
||||
await folderAHeader.click();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 2')).not.toBeVisible();
|
||||
|
||||
// Should not delete when the editor is focused
|
||||
await mainScreen.noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('test');
|
||||
await pressShiftDelete();
|
||||
|
||||
await folderBHeader.click();
|
||||
await folderAHeader.click();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import waitForNextOpenPath from './util/waitForNextOpenPath';
|
||||
import { basename } from 'path';
|
||||
|
||||
test.describe('richTextEditor', () => {
|
||||
test('HTML links should be preserved when editing a note', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('Testing!');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
// Set the note's content
|
||||
await editor.focusCodeMirrorEditor();
|
||||
|
||||
// Attach this file to the note (create a resource ID)
|
||||
await setFilePickerResponse(electronApp, [__filename]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
// Wait to render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await viewerFrame.locator('a[data-from-md]').waitFor();
|
||||
|
||||
// Should have an attached resource
|
||||
const codeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
|
||||
const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/;
|
||||
expect(codeMirrorContent).toMatch(resourceUrlExpression);
|
||||
const resourceId = codeMirrorContent.match(resourceUrlExpression)[1];
|
||||
|
||||
// Create a new note with just an HTML link
|
||||
await mainScreen.createNewNote('Another test');
|
||||
await editor.codeMirrorEditor.click();
|
||||
await mainWindow.keyboard.type(`<a href=":/${resourceId}">HTML Link</a>`);
|
||||
|
||||
// Switch to the RTE
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.richTextEditor.waitFor();
|
||||
|
||||
// Edit the note to cause the original content to update
|
||||
await editor.getTinyMCEFrameLocator().locator('a').click();
|
||||
await mainWindow.keyboard.type('Test...');
|
||||
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.codeMirrorEditor.waitFor();
|
||||
|
||||
// Note should still contain the resource ID and note title
|
||||
const finalCodeMirrorContent = await editor.codeMirrorEditor.innerText();
|
||||
expect(finalCodeMirrorContent).toContain(`:/${resourceId}`);
|
||||
});
|
||||
|
||||
test('should watch resources for changes when opened with ctrl+click', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('Testing!');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
// Set the note's content
|
||||
await editor.focusCodeMirrorEditor();
|
||||
|
||||
// Attach this file to the note (create a resource ID)
|
||||
const pathToAttach = __filename;
|
||||
await setFilePickerResponse(electronApp, [pathToAttach]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
// Switch to the RTE
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.richTextEditor.waitFor();
|
||||
|
||||
await editor.richTextEditor.click();
|
||||
|
||||
// Click on the attached file URL
|
||||
const openPathResult = waitForNextOpenPath(electronApp);
|
||||
const targetLink = editor.getTinyMCEFrameLocator().getByRole('link', { name: basename(pathToAttach) });
|
||||
if (process.platform === 'darwin') {
|
||||
await targetLink.click({ modifiers: ['Meta'] });
|
||||
} else {
|
||||
await targetLink.click({ modifiers: ['Control'] });
|
||||
}
|
||||
|
||||
// Should watch the file
|
||||
await mainWindow.getByText(/^The following attachments are being watched for changes/i).waitFor();
|
||||
expect(await openPathResult).toContain(basename(pathToAttach));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
|
||||
const waitForNextOpenPath = (electronApp: ElectronApplication) => {
|
||||
return electronApp.evaluate(async ({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const originalOpenPath = shell.openPath;
|
||||
shell.openPath = async (path: string) => {
|
||||
shell.openPath = originalOpenPath;
|
||||
resolve(path);
|
||||
return '';
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default waitForNextOpenPath;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.13",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -126,8 +126,8 @@
|
||||
"@playwright/test": "1.42.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.14",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/react": "18.2.58",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
@@ -137,7 +137,7 @@
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"js-sha512": "0.8.0",
|
||||
"js-sha512": "0.9.0",
|
||||
"nan": "2.19.0",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"ts-node": "10.9.2",
|
||||
@@ -192,7 +192,7 @@
|
||||
"styled-components": "5.3.11",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.0.4",
|
||||
"tesseract.js": "5.0.5",
|
||||
"tinymce": "5.10.6"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby '>= 2.6.10'
|
||||
gem 'cocoapods', '>= 1.11.3'
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Cocoapods 1.15 introduced a bug which breaks the build. We will remove the upper
|
||||
# bound in the template on Cocoapods with next React Native release.
|
||||
gem 'cocoapods', '>= 1.13', '< 1.15'
|
||||
gem 'activesupport', '>= 6.1.7.5', '< 7.1.0'
|
||||
@@ -1,28 +1,28 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
/* This is the configuration block to customize your React Native Android app.
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
|
||||
// codegenDir = file("../node_modules/react-native-codegen")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
@@ -42,6 +42,7 @@ react {
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
@@ -50,14 +51,6 @@ react {
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to create four separate APKs instead of one,
|
||||
* one for each native architecture. This is useful if you don't
|
||||
* use App Bundles (https://developer.android.com/guide/app-bundle/)
|
||||
* and want to have separate APKs to upload to the Play Store.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
@@ -74,44 +67,20 @@ def enableProguardInReleaseBuilds = false
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
|
||||
// We need the intl variant to support natural sorting of notes.
|
||||
// https://github.com/laurent22/joplin/pull/4272
|
||||
def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
|
||||
/**
|
||||
* Private function to get the list of Native Architectures you want to build.
|
||||
* This reads the value from reactNativeArchitectures in your gradle.properties
|
||||
* file and works together with the --active-arch-only flag of react-native run-android.
|
||||
*/
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||
}
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// dexOptions {
|
||||
// // To fix "GC overhead limit exceeded"
|
||||
// // https://stackoverflow.com/q/32133013/561309
|
||||
// javaMaxHeapSize "4g"
|
||||
// }
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "net.cozic.joplin"
|
||||
defaultConfig {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097742
|
||||
versionName "3.0.3"
|
||||
versionCode 2097747
|
||||
versionName "3.0.8"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
@@ -120,17 +89,7 @@ android {
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
|
||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
// include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
include (*reactNativeArchitectures())
|
||||
}
|
||||
multiDexEnabled true
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -147,7 +106,7 @@ android {
|
||||
keyPassword JOPLIN_RELEASE_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
@@ -160,32 +119,6 @@ android {
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// To fix this error:
|
||||
//
|
||||
// > Execution failed for task ':app:lintVitalRelease'
|
||||
//
|
||||
// https://stackoverflow.com/a/62603296/561309
|
||||
// lintOptions {
|
||||
// disable 'InvalidPackage'
|
||||
// checkReleaseBuilds false
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -195,27 +128,14 @@ dependencies {
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-vision'
|
||||
}
|
||||
|
||||
// implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||
// implementation 'com.android.support:multidex:2.0.1'
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
@@ -7,7 +7,5 @@
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
|
||||
</application>
|
||||
tools:ignore="GoogleAppIndexingWarning"/>
|
||||
</manifest>
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package net.cozic.joplin;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,15 @@
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<!-- /SHARE EXTENSION -->
|
||||
|
||||
<!-- EXTERNAL LINK -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="joplin" />
|
||||
</intent-filter>
|
||||
<!-- /EXTERNAL LINK -->
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package net.cozic.joplin;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "Joplin";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.cozic.joplin
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "Joplin"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package net.cozic.joplin;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.CursorWindow;
|
||||
import android.webkit.WebView;
|
||||
|
||||
// import androidx.multidex.MultiDex;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.oblador.vectoricons.VectorIconsPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import net.cozic.joplin.share.SharePackage;
|
||||
import net.cozic.joplin.ssl.SslPackage;
|
||||
import net.cozic.joplin.textinput.TextInputPackage;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||
// @Override
|
||||
// protected void attachBaseContext(Context base) {
|
||||
// super.attachBaseContext(base);
|
||||
// MultiDex.install(this);
|
||||
// }
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
packages.add(new SharePackage());
|
||||
packages.add(new SslPackage());
|
||||
packages.add(new TextInputPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// To try to fix the error "Row too big to fit into CursorWindow"
|
||||
// https://github.com/andpor/react-native-sqlite-storage/issues/364#issuecomment-526423153
|
||||
// https://github.com/laurent22/joplin/issues/1767#issuecomment-515617991
|
||||
try {
|
||||
Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
|
||||
field.setAccessible(true);
|
||||
field.set(null, 50 * 1024 * 1024); //the 102400 is the new size added
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.cozic.joplin
|
||||
|
||||
import android.app.Application
|
||||
import android.database.CursorWindow
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.soloader.SoLoader
|
||||
import net.cozic.joplin.versioninfo.SystemVersionInformationPackage
|
||||
import net.cozic.joplin.share.SharePackage
|
||||
import net.cozic.joplin.ssl.SslPackage
|
||||
import net.cozic.joplin.textinput.TextInputPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
add(SharePackage())
|
||||
add(SslPackage())
|
||||
add(TextInputPackage())
|
||||
add(SystemVersionInformationPackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// To try to fix the error "Row too big to fit into CursorWindow"
|
||||
// https://github.com/andpor/react-native-sqlite-storage/issues/364#issuecomment-526423153
|
||||
// https://github.com/laurent22/joplin/issues/1767#issuecomment-515617991
|
||||
try {
|
||||
val field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize")
|
||||
field.isAccessible = true
|
||||
field[null] = 50 * 1024 * 1024 //the 102400 is the new size added
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
SoLoader.init(this, /* native exopackage */false)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.cozic.joplin.versioninfo;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.Build;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SystemVersionInformationPackage implements ReactPackage {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.singletonList(new SystemVersionInformationModule(reactContext));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static class SystemVersionInformationModule extends ReactContextBaseJavaModule {
|
||||
public SystemVersionInformationModule(@NonNull ReactApplicationContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> result = new HashMap<String, Object>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PackageInfo webViewPackage = WebView.getCurrentWebViewPackage();
|
||||
|
||||
if (webViewPackage != null) {
|
||||
result.put("webViewVersion", webViewPackage.versionName);
|
||||
result.put("webViewPackage", webViewPackage.packageName);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SystemVersionInformationModule";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
<selector>
|
||||
<!--
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package net.cozic.joplin;
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
buildToolsVersion = "34.0.0"
|
||||
minSdkVersion = 23
|
||||
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
ndkVersion = "26.1.10909125"
|
||||
kotlinVersion = "1.9.22"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +55,5 @@ allprojects {
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
@@ -25,9 +25,6 @@ android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.125.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -55,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,13 +80,11 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -133,22 +131,29 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
@@ -205,6 +214,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
@@ -231,4 +246,4 @@ eval "set -- $(
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -54,48 +55,36 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -3,4 +3,4 @@ include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
@@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as newNote from './newNote';
|
||||
import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as scrollToHash from './scrollToHash';
|
||||
|
||||
const index: any[] = [
|
||||
newNote,
|
||||
openItem,
|
||||
openNote,
|
||||
scrollToHash,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { runtime } from './newNote';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
describe('newNote', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
test('should create and navigate to a new note', async () => {
|
||||
const dispatchMock = jest.fn();
|
||||
NavService.dispatch = dispatchMock;
|
||||
|
||||
// The command needs an active folder ID.
|
||||
const activeFolder = await Folder.save({ title: 'folder' });
|
||||
Setting.setValue('activeFolderId', activeFolder.id);
|
||||
|
||||
await runtime().execute(null, 'test note', true);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Correct note should have been created
|
||||
const noteId = dispatchMock.mock.lastCall[0].noteId;
|
||||
expect(await Note.load(noteId)).toMatchObject({ body: 'test note', parent_id: activeFolder.id });
|
||||
|
||||
// Should have tried to navigate to the note.
|
||||
expect(dispatchMock.mock.lastCall).toMatchObject([
|
||||
{ noteId: noteId, noteHash: '' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import goToNote from './util/goToNote';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const logger = Logger.create('newNoteCommand');
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newNote',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, body = '', todo = false) => {
|
||||
const folderId = Setting.value('activeFolderId');
|
||||
if (!folderId) {
|
||||
logger.warn('Not creating new note -- no active folder ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await Note.save({
|
||||
body,
|
||||
parent_id: folderId,
|
||||
is_todo: todo ? 1 : 0,
|
||||
}, { provisional: true });
|
||||
|
||||
logger.info(`Navigating to note ${note.id}`);
|
||||
await goToNote(note.id, '');
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { parseResourceUrl, urlProtocol } = require('@joplin/lib/urlUtils');
|
||||
import { parseResourceUrl, urlProtocol } from '@joplin/lib/urlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import goToNote from './util/goToNote';
|
||||
|
||||
|
||||