You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
208 Commits
mobile_plu
...
android-v3
Author | SHA1 | Date | |
---|---|---|---|
|
7a6a4e118a | ||
|
b7a652fb71 | ||
|
821558fe30 | ||
|
5280ec12cd | ||
|
a29e30e442 | ||
|
fe88703488 | ||
|
338dabf5da | ||
|
59447f4c45 | ||
|
04196e4485 | ||
|
d4fafd74d2 | ||
|
f185480ceb | ||
|
2e73ea2d9f | ||
|
8ac19d80ea | ||
|
911689a4ac | ||
|
da0a7faf12 | ||
|
9be533a19e | ||
|
050871bc65 | ||
|
8d6d7ca6d2 | ||
|
b8c88af82d | ||
|
e17ef72111 | ||
|
287e0da4b4 | ||
|
471f5a72fe | ||
|
1e5c41dc48 | ||
|
716e5252c1 | ||
|
f10fd4b2da | ||
|
ff4d18dec3 | ||
|
020cd914af | ||
|
8dcfc81cee | ||
|
77048caeeb | ||
|
3438c58ec4 | ||
|
e74d5e7c23 | ||
|
c921976e9d | ||
|
c8a2802181 | ||
|
0f08688ce8 | ||
|
f5f7b1eb60 | ||
|
9d73e583b9 | ||
|
611e8df81a | ||
|
b0c9c4c8ce | ||
|
c524f5a6b5 | ||
|
ee2b186752 | ||
|
76a3250707 | ||
|
e8144f9ee2 | ||
|
47f6b1ce33 | ||
|
f0121e7799 | ||
|
0acb14d0bf | ||
|
18ebd16428 | ||
|
6bc1965ec0 | ||
|
1fed875140 | ||
|
98fe57e87a | ||
|
5bcb2531f4 | ||
|
f6d69ef702 | ||
|
1f05a3212f | ||
|
ff0321e906 | ||
|
6b881b226e | ||
|
6a26ec8105 | ||
|
5966402d8b | ||
|
5198b598bb | ||
|
0a4c97c631 | ||
|
0bc62aa05e | ||
|
44d1e9e3ca | ||
|
0cef6cc611 | ||
|
3e5acfbc09 | ||
|
675f55d152 | ||
|
b1edb84b49 | ||
|
ece7a4ccf0 | ||
|
cfd98d2723 | ||
|
2a17301a9f | ||
|
e3762dc3f8 | ||
|
0959a19d65 | ||
|
93219575b4 | ||
|
baaeea1307 | ||
|
de6c5d448f | ||
|
cc2cf5f521 | ||
|
ef513862a9 | ||
|
d07f3b5f16 | ||
|
a6079869bc | ||
|
2fdbb22481 | ||
|
c5bb88ddf4 | ||
|
5d7c78c361 | ||
|
808eb7d49a | ||
|
2142373fff | ||
|
20f7f37b49 | ||
|
04fc634092 | ||
|
d40c9d3ff9 | ||
|
224b4f619a | ||
|
88d1d4b7d1 | ||
|
6a22ffbcb1 | ||
|
d735cf64e0 | ||
|
d7d6fd5ccd | ||
|
23254e6ffd | ||
|
eb8bfd5aec | ||
|
cb5ffd968d | ||
|
7b2b3a4f80 | ||
|
cbfe109c41 | ||
|
c8b01d11d6 | ||
|
b042395fd1 | ||
|
ba5ad18093 | ||
|
ff15232a10 | ||
|
5a6e72197a | ||
|
de555b6871 | ||
|
9a2548a5e3 | ||
|
107996289f | ||
|
c3c0101555 | ||
|
64f3dae8cc | ||
|
a39b51cc97 | ||
|
10bb8ef1a9 | ||
|
60ba22b233 | ||
|
1bfd997be2 | ||
|
81e4a7fb74 | ||
|
360568d325 | ||
|
1aa0f11670 | ||
|
0430ccb3e7 | ||
|
c0d6c1eb0b | ||
|
215f09d73c | ||
|
1f192696de | ||
|
ab86b95fad | ||
|
0f07c0f53a | ||
|
a6d04c4781 | ||
|
bc27f47881 | ||
|
d1d75449f5 | ||
|
bbea5388ed | ||
|
99e773855e | ||
|
55b73347e5 | ||
|
7e8dee4906 | ||
|
69fb1ab104 | ||
|
67ae0ea2d1 | ||
|
cdb61b922b | ||
|
da80443796 | ||
|
1924dd31d2 | ||
|
b831d8c068 | ||
|
4ad1b49769 | ||
|
0d6c1067e3 | ||
|
0bdc38a6be | ||
|
5c35569b5b | ||
|
5f02af9724 | ||
|
975f16d21c | ||
|
06359834d6 | ||
|
0cc0fec8c3 | ||
|
68ab5dcda5 | ||
|
65544123e6 | ||
|
cfbded00e2 | ||
|
a898e17b4c | ||
|
d12e2d9a81 | ||
|
7025321d76 | ||
|
6c890121b9 | ||
|
9c4be00745 | ||
|
7f51712311 | ||
|
502c929c88 | ||
|
1abf9e9602 | ||
|
8bdb6c5d72 | ||
|
9cbd1b855c | ||
|
ae8658554f | ||
|
bc385d59e9 | ||
|
00ccd994e3 | ||
|
9251299289 | ||
|
fe67a44285 | ||
|
50a1b184fd | ||
|
3caa718132 | ||
|
d0e16c0878 | ||
|
4fcb250c27 | ||
|
86e59ad621 | ||
|
12baa9827d | ||
|
95c50ada7c | ||
|
55a57f7baf | ||
|
69b24b4437 | ||
|
5143fae0f6 | ||
|
01a62acfdf | ||
|
c663742689 | ||
|
0c405951ed | ||
|
4b411e600c | ||
|
bf58a52394 | ||
|
36d3736bff | ||
|
4df0b9f851 | ||
|
914b5e230d | ||
|
9278fd7910 | ||
|
2180ad1d9b | ||
|
d301cdf992 | ||
|
200d3c84e0 | ||
|
6cadaa2137 | ||
|
8221081514 | ||
|
dd06b1e680 | ||
|
70e0ae0c2c | ||
|
7aeec923e3 | ||
|
70d64225c8 | ||
|
ad0ecc2320 | ||
|
8a28edcda8 | ||
|
c8640aa7f8 | ||
|
ddf75d6c52 | ||
|
0a42317e07 | ||
|
51ce1b06fe | ||
|
44c735afac | ||
|
c6154cfb4e | ||
|
d2aad1d6c7 | ||
|
3e81cc8585 | ||
|
abc5c062c3 | ||
|
316ef9d960 | ||
|
b870f8344c | ||
|
6f6683d15d | ||
|
da59aef95b | ||
|
c55979cd03 | ||
|
07f4217f17 | ||
|
8a7071179d | ||
|
2c9a12307e | ||
|
dd3864fa47 | ||
|
43c1c5849b | ||
|
5e08ff0621 | ||
|
45838c0223 | ||
|
17e463b6bc |
@@ -158,6 +158,7 @@ packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/newAppInstance.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
@@ -272,6 +273,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
@@ -353,13 +355,16 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/types.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -421,6 +426,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
@@ -436,6 +442,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||
@@ -456,7 +463,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
@@ -465,7 +471,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
@@ -582,6 +587,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
@@ -591,6 +597,7 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
@@ -684,15 +691,21 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/types.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
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/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -739,8 +752,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
|
||||
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/SettingTextInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.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/InstalledPluginBox.js
|
||||
@@ -780,7 +796,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -789,12 +807,16 @@ packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
packages/app-mobile/components/voiceTyping/RecordingControls.js
|
||||
packages/app-mobile/components/voiceTyping/SpeechToTextBanner.js
|
||||
packages/app-mobile/components/voiceTyping/types.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
@@ -808,12 +830,11 @@ packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.test.js
|
||||
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
@@ -832,6 +853,7 @@ packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/focusView.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||
@@ -844,9 +866,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -954,6 +977,7 @@ packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -1021,7 +1045,11 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
@@ -1057,6 +1085,8 @@ packages/lib/fs-driver-base.js
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
@@ -1115,6 +1145,9 @@ packages/lib/models/settings/builtInMetadata.js
|
||||
packages/lib/models/settings/settingValidations.test.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.test.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -1257,6 +1290,7 @@ packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
packages/lib/services/plugins/BasePlatformImplementation.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/EditorPluginHandler.js
|
||||
packages/lib/services/plugins/MenuController.js
|
||||
packages/lib/services/plugins/MenuItemController.js
|
||||
packages/lib/services/plugins/Plugin.js
|
||||
@@ -1303,6 +1337,7 @@ packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
@@ -57,6 +57,8 @@ module.exports = {
|
||||
'tinymce': 'readonly',
|
||||
|
||||
'JSX': 'readonly',
|
||||
|
||||
'NodeJS': 'readonly',
|
||||
},
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
@@ -309,7 +311,7 @@ module.exports = {
|
||||
selector: 'interface',
|
||||
format: null,
|
||||
'filter': {
|
||||
'regex': '^(RSA|RSAKeyPair)$',
|
||||
'regex': '^(RSA|RSAKeyPair|iOS.*)$',
|
||||
'match': true,
|
||||
},
|
||||
},
|
||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://discourse.joplinapp.org/c/features/
|
||||
about: Discuss ideas for new features or changes
|
||||
- name: Support
|
||||
url: https://discourse.joplinapp.org/c/support/
|
||||
about: Please ask for help here
|
||||
about: Please ask for help here
|
||||
|
34
.github/scripts/publish_docker_manifest.sh
vendored
Normal file
34
.github/scripts/publish_docker_manifest.sh
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=$(echo "$GIT_TAG_NAME" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
|
||||
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "VERSION=$VERSION"
|
||||
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
|
||||
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
|
||||
|
||||
# Check if it's a server release, otherwise exit
|
||||
if [[ $GIT_TAG_NAME != $SERVER_TAG_PREFIX-* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest inspect $SERVER_REPOSITORY:arm64-$VERSION > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Image $SERVER_REPOSITORY:arm64-$VERSION does not exist on the remote registry."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest inspect $SERVER_REPOSITORY:amd64-$VERSION > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Image $SERVER_REPOSITORY:amd64-$VERSION does not exist on the remote registry."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest create $SERVER_REPOSITORY:$VERSION \
|
||||
$SERVER_REPOSITORY:arm64-$VERSION \
|
||||
$SERVER_REPOSITORY:amd64-$VERSION
|
||||
|
||||
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
|
||||
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
|
||||
|
||||
docker manifest push $SERVER_REPOSITORY:$VERSION
|
28
.github/scripts/run_ci.sh
vendored
28
.github/scripts/run_ci.sh
vendored
@@ -35,6 +35,8 @@ else
|
||||
IS_MACOS=1
|
||||
fi
|
||||
|
||||
DOCKER_IMAGE_PLATFORM="linux/amd64"
|
||||
|
||||
# Tests can randomly fail in some cases, so only run them when not publishing
|
||||
# a release
|
||||
RUN_TESTS=0
|
||||
@@ -43,10 +45,33 @@ if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ]; then
|
||||
RUN_TESTS=1
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ] && [ "$IS_SERVER_RELEASE" == "0" ]; then
|
||||
# We exit now because nothing works properly with the ARM64 architecture.
|
||||
# We only proceed if building the server image.
|
||||
echo "Running on ARM64 and not trying to build server image - early exit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
|
||||
RUN_TESTS=0
|
||||
cd "$ROOT_DIR/packages/lib"
|
||||
yarn remove canvas
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
DOCKER_IMAGE_PLATFORM="linux/arm64"
|
||||
|
||||
# Delete certain directories because `yarn install` will fail on ARM64.
|
||||
rm -rf app-desktop
|
||||
rm -rf app-mobile
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Print environment
|
||||
# =============================================================================
|
||||
|
||||
echo "RUNNER_OS=$RUNNER_OS"
|
||||
echo "RUNNER_ARCH=$RUNNER_ARCH"
|
||||
echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW"
|
||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||
echo "GITHUB_REF=$GITHUB_REF"
|
||||
@@ -55,6 +80,7 @@ echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
|
||||
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
|
||||
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
|
||||
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
|
||||
@@ -277,7 +303,7 @@ if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
|
||||
elif [[ $IS_LINUX = 1 ]] && [ "$IS_SERVER_RELEASE" == "1" ]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
else
|
||||
echo "Step: Building but *not* publishing desktop application..."
|
||||
|
||||
|
107
.github/workflows/github-actions-main.yml
vendored
107
.github/workflows/github-actions-main.yml
vendored
@@ -9,47 +9,12 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-13, ubuntu-20.04, windows-2019]
|
||||
os: [macos-13, ubuntu-22.04, windows-2019, ubuntu-22.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Trying to fix random networking issues on Windows
|
||||
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
|
||||
- name: Disable TCP/UDP offload on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
|
||||
|
||||
- name: Disable TCP/UDP offload on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Disable TCP/UDP offload on macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't
|
||||
# exist) since otherwise it will make the whole build fails, even though
|
||||
# it might work without update. libsecret-1-dev is required for keytar -
|
||||
# https://github.com/atom/node-keytar
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y gettext
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install macOs dependencies
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
# Required for building the canvas package
|
||||
brew install pango
|
||||
- name: Setup build environment
|
||||
uses: ./.github/workflows/shared/setup-build-environment
|
||||
|
||||
- name: Install Docker Engine
|
||||
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
@@ -62,26 +27,11 @@ jobs:
|
||||
sudo apt-get install -y lsb-release
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo \
|
||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
corepack enable
|
||||
|
||||
# Login to Docker only if we're on a server release tag. If we run this on
|
||||
# a pull request it will fail because the PR doesn't have access to
|
||||
# secrets
|
||||
@@ -91,15 +41,6 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# macos-latest ships with Python 3.12 by default, but this removes a
|
||||
# utility that's used by electron-builder (distutils) so we need to pin
|
||||
# Python to an earlier version.
|
||||
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Run tests, build and publish Linux and macOS apps
|
||||
if: runner.os == 'Linux' || runner.os == 'macOs'
|
||||
env:
|
||||
@@ -143,6 +84,15 @@ jobs:
|
||||
run: |
|
||||
yarn install && cd packages/app-desktop && yarn dist --publish=never
|
||||
|
||||
- name: Publish Docker manifest
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
SERVER_REPOSITORY: joplin/server
|
||||
SERVER_TAG_PREFIX: server
|
||||
run: |
|
||||
chmod 700 "${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
|
||||
|
||||
ServerDockerImage:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -150,7 +100,7 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [ubuntu-20.04]
|
||||
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||
steps:
|
||||
|
||||
- name: Install Docker Engine
|
||||
@@ -162,10 +112,10 @@ jobs:
|
||||
sudo apt-get install -y lsb-release
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo \
|
||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -183,17 +133,30 @@ jobs:
|
||||
env:
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
DOCKER_IMAGE_PLATFORM="linux/arm64"
|
||||
fi
|
||||
|
||||
echo "RUNNER_OS=$RUNNER_OS"
|
||||
echo "RUNNER_ARCH=$RUNNER_ARCH"
|
||||
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
|
||||
|
||||
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
|
||||
cd packages/lib
|
||||
yarn remove canvas
|
||||
cd ../..
|
||||
|
||||
yarn install
|
||||
yarn buildServerDocker --tag-name server-v0.0.0 --repository joplin/server
|
||||
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
|
||||
|
||||
# Basic test to ensure that the created build is valid. It should exit with
|
||||
# code 0 if it works.
|
||||
docker run joplin/server:0.0.0-beta node dist/app.js migrate list
|
||||
# code 0 if it works.
|
||||
docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js migrate list
|
||||
|
||||
- name: Check HTTP request
|
||||
run: |
|
||||
# Need to pass environment variables:
|
||||
docker run -p 22300:22300 joplin/server:0.0.0-beta node dist/app.js --env dev &
|
||||
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
|
||||
# Wait for server to start
|
||||
sleep 30
|
||||
|
72
.github/workflows/shared/setup-build-environment/action.yml
vendored
Normal file
72
.github/workflows/shared/setup-build-environment/action.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: 'Setup build environment'
|
||||
description: 'Install Joplin build dependencies'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
# Trying to fix random networking issues on Windows
|
||||
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
|
||||
- name: Disable TCP/UDP offload on Windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
|
||||
|
||||
- name: Disable TCP/UDP offload on Linux
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Disable TCP/UDP offload on macOS
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't
|
||||
# exist) since otherwise it will make the whole build fails, even though
|
||||
# it might work without update. libsecret-1-dev is required for keytar -
|
||||
# https://github.com/atom/node-keytar
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y gettext
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install macOs dependencies
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
# Required for building the canvas package
|
||||
brew install pango
|
||||
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
shell: bash
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
corepack enable
|
||||
|
||||
# macos-latest ships with Python 3.12 by default, but this removes a
|
||||
# utility that's used by electron-builder (distutils) so we need to pin
|
||||
# Python to an earlier version.
|
||||
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
29
.github/workflows/ui-tests.yml
vendored
Normal file
29
.github/workflows/ui-tests.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Joplin UI tests
|
||||
on: [push, pull_request]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
Main:
|
||||
# Don't run on forks
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-13, ubuntu-22.04]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup build environment
|
||||
uses: ./.github/workflows/shared/setup-build-environment
|
||||
- name: Build
|
||||
run: yarn install
|
||||
- name: Run UI tests
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}/packages/app-desktop/
|
||||
bash ./integration-tests/run-ci.sh
|
||||
# See https://playwright.dev/docs/ci-intro#setting-up-github-actions
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}
|
||||
path: packages/app-desktop/playwright-report/
|
||||
retention-days: 7
|
55
.gitignore
vendored
55
.gitignore
vendored
@@ -133,6 +133,7 @@ packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/newAppInstance.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
@@ -247,6 +248,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
@@ -328,13 +330,16 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/types.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -396,6 +401,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
@@ -411,6 +417,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||
@@ -431,7 +438,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
@@ -440,7 +446,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
@@ -557,6 +562,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
@@ -566,6 +572,7 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
@@ -659,15 +666,21 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/types.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
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/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -714,8 +727,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
|
||||
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/SettingTextInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.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/InstalledPluginBox.js
|
||||
@@ -755,7 +771,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -764,12 +782,16 @@ packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
packages/app-mobile/components/voiceTyping/RecordingControls.js
|
||||
packages/app-mobile/components/voiceTyping/SpeechToTextBanner.js
|
||||
packages/app-mobile/components/voiceTyping/types.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
@@ -783,12 +805,11 @@ packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.test.js
|
||||
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
@@ -807,6 +828,7 @@ packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/focusView.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||
@@ -819,9 +841,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -929,6 +952,7 @@ packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
|
||||
packages/editor/CodeMirror/utils/overwriteModeExtension.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -996,7 +1020,11 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
@@ -1032,6 +1060,8 @@ packages/lib/fs-driver-base.js
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
@@ -1090,6 +1120,9 @@ packages/lib/models/settings/builtInMetadata.js
|
||||
packages/lib/models/settings/settingValidations.test.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.test.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -1232,6 +1265,7 @@ packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
packages/lib/services/plugins/BasePlatformImplementation.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/EditorPluginHandler.js
|
||||
packages/lib/services/plugins/MenuController.js
|
||||
packages/lib/services/plugins/MenuItemController.js
|
||||
packages/lib/services/plugins/Plugin.js
|
||||
@@ -1278,6 +1312,7 @@ packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
@@ -1,62 +0,0 @@
|
||||
diff --git a/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java b/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
index a5bb95eec3337b93a2338a2869a2bda176c91cae..87817688eb280c2f702c26dc35558c6a0a4db1ea 100644
|
||||
--- a/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
+++ b/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
@@ -42,12 +42,20 @@ public class ReactSliderManager extends SimpleViewManager<ReactSlider> implement
|
||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
||||
ReactSlider slider = (ReactSlider)seekbar;
|
||||
|
||||
- if(progress < slider.getLowerLimit()) {
|
||||
- progress = slider.getLowerLimit();
|
||||
- seekbar.setProgress(progress);
|
||||
- } else if (progress > slider.getUpperLimit()) {
|
||||
- progress = slider.getUpperLimit();
|
||||
- seekbar.setProgress(progress);
|
||||
+ // During initialization, lowerLimit can be greater than upperLimit.
|
||||
+ //
|
||||
+ // If a change event is received during this, we need a check to prevent
|
||||
+ // infinite recursion.
|
||||
+ //
|
||||
+ // Issue: https://github.com/callstack/react-native-slider/issues/571
|
||||
+ if (slider.getLowerLimit() <= slider.getUpperLimit()) {
|
||||
+ if(progress < slider.getLowerLimit()) {
|
||||
+ progress = slider.getLowerLimit();
|
||||
+ seekbar.setProgress(progress);
|
||||
+ } else if (progress > slider.getUpperLimit()) {
|
||||
+ progress = slider.getUpperLimit();
|
||||
+ seekbar.setProgress(progress);
|
||||
+ }
|
||||
}
|
||||
|
||||
ReactContext reactContext = (ReactContext) seekbar.getContext();
|
||||
diff --git a/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java b/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
index 3ff5930f85a3cd92c2549925f41058abb188a57e..ab3681fdfe0b736c97020e1434e450c8183e6f18 100644
|
||||
--- a/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
+++ b/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
|
||||
@@ -30,12 +30,20 @@ public class ReactSliderManager extends SimpleViewManager<ReactSlider> {
|
||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
||||
ReactSlider slider = (ReactSlider)seekbar;
|
||||
|
||||
- if(progress < slider.getLowerLimit()) {
|
||||
- progress = slider.getLowerLimit();
|
||||
- seekbar.setProgress(progress);
|
||||
- } else if(progress > slider.getUpperLimit()) {
|
||||
- progress = slider.getUpperLimit();
|
||||
- seekbar.setProgress(progress);
|
||||
+ // During initialization, lowerLimit can be greater than upperLimit.
|
||||
+ //
|
||||
+ // If a change event is received during this, we need a check to prevent
|
||||
+ // infinite recursion.
|
||||
+ //
|
||||
+ // Issue: https://github.com/callstack/react-native-slider/issues/571
|
||||
+ if (slider.getLowerLimit() <= slider.getUpperLimit()) {
|
||||
+ if(progress < slider.getLowerLimit()) {
|
||||
+ progress = slider.getLowerLimit();
|
||||
+ seekbar.setProgress(progress);
|
||||
+ } else if (progress > slider.getUpperLimit()) {
|
||||
+ progress = slider.getUpperLimit();
|
||||
+ seekbar.setProgress(progress);
|
||||
+ }
|
||||
}
|
||||
|
||||
ReactContext reactContext = (ReactContext) seekbar.getContext();
|
50
.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch
Normal file
50
.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch
Normal file
@@ -0,0 +1,50 @@
|
||||
# This is a (hopefully temporary) fix for an accessibility issue in the FAB.Group
|
||||
# component. See https://github.com/callstack/react-native-paper/pull/4498 for details.
|
||||
diff --git a/lib/commonjs/components/FAB/FABGroup.js b/lib/commonjs/components/FAB/FABGroup.js
|
||||
index 26933dd7ac6862c0dd95e52b8cd91c8bbd0b6efc..417c91a0257849eb597afb5e339e13b6d1d54486 100644
|
||||
--- a/lib/commonjs/components/FAB/FABGroup.js
|
||||
+++ b/lib/commonjs/components/FAB/FABGroup.js
|
||||
@@ -209,8 +209,9 @@ const FABGroup = _ref => {
|
||||
}],
|
||||
pointerEvents: open ? 'box-none' : 'none',
|
||||
accessibilityRole: "button",
|
||||
- importantForAccessibility: "yes",
|
||||
- accessible: true,
|
||||
+ importantForAccessibility: open ? 'yes' : 'no-hide-descendants',
|
||||
+ accessibilityElementsHidden: !open,
|
||||
+ accessible: open,
|
||||
accessibilityLabel: accessibilityLabel
|
||||
}, it.label && /*#__PURE__*/React.createElement(_reactNative.View, null, /*#__PURE__*/React.createElement(_Card.default, {
|
||||
mode: isV3 ? 'contained' : 'elevated',
|
||||
diff --git a/lib/module/components/FAB/FABGroup.js b/lib/module/components/FAB/FABGroup.js
|
||||
index ca5c02679539b17b048d4c82f570791dd8b57545..a06902b744b3bfb06b0644930eda0ba2ce2967ca 100644
|
||||
--- a/lib/module/components/FAB/FABGroup.js
|
||||
+++ b/lib/module/components/FAB/FABGroup.js
|
||||
@@ -200,8 +200,9 @@ const FABGroup = _ref => {
|
||||
}],
|
||||
pointerEvents: open ? 'box-none' : 'none',
|
||||
accessibilityRole: "button",
|
||||
- importantForAccessibility: "yes",
|
||||
- accessible: true,
|
||||
+ importantForAccessibility: open ? 'yes' : 'no-hide-descendants',
|
||||
+ accessibilityElementsHidden: !open,
|
||||
+ accessible: open,
|
||||
accessibilityLabel: accessibilityLabel
|
||||
}, it.label && /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Card, {
|
||||
mode: isV3 ? 'contained' : 'elevated',
|
||||
diff --git a/src/components/FAB/FABGroup.tsx b/src/components/FAB/FABGroup.tsx
|
||||
index af1e85c4cbabfdd05499f9befb9f851be5911835..d010393975b0b31852efba1b7ce9cb09da4feaec 100644
|
||||
--- a/src/components/FAB/FABGroup.tsx
|
||||
+++ b/src/components/FAB/FABGroup.tsx
|
||||
@@ -383,8 +383,9 @@ const FABGroup = ({
|
||||
]}
|
||||
pointerEvents={open ? 'box-none' : 'none'}
|
||||
accessibilityRole="button"
|
||||
- importantForAccessibility="yes"
|
||||
- accessible={true}
|
||||
+ importantForAccessibility={open ? 'yes' : 'no-hide-descendants'}
|
||||
+ accessibilityElementsHidden={!open}
|
||||
+ accessible={open}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{it.label && (
|
@@ -0,0 +1,55 @@
|
||||
# This patch improves the note actions menu (the kebab menu)'s accessibility
|
||||
# by labelling its dismiss button.
|
||||
diff --git a/build/rnpm.js b/build/rnpm.js
|
||||
index 1111c2de99b3d4c5651ca4eee3ba59c0ce8e13e1..d410ee12b38d02c399b0a40973217da0082d73c0 100644
|
||||
--- a/build/rnpm.js
|
||||
+++ b/build/rnpm.js
|
||||
@@ -1573,7 +1573,9 @@
|
||||
onPress = _this$props.onPress,
|
||||
style = _this$props.style;
|
||||
return /*#__PURE__*/React__default.createElement(reactNative.TouchableWithoutFeedback, {
|
||||
- onPress: onPress
|
||||
+ onPress: onPress,
|
||||
+ accessibilityLabel: _this$props.accessibilityLabel,
|
||||
+ accessibilityRole: 'button',
|
||||
}, /*#__PURE__*/React__default.createElement(reactNative.Animated.View, {
|
||||
style: [styles.fullscreen, {
|
||||
opacity: this.fadeAnim
|
||||
@@ -1588,7 +1590,8 @@
|
||||
}(React.Component);
|
||||
|
||||
Backdrop.propTypes = {
|
||||
- onPress: propTypes.func.isRequired
|
||||
+ onPress: propTypes.func.isRequired,
|
||||
+ accessibilityLabel: propTypes.string,
|
||||
};
|
||||
var styles = reactNative.StyleSheet.create({
|
||||
fullscreen: {
|
||||
@@ -1658,6 +1661,7 @@
|
||||
style: styles$1.placeholder
|
||||
}, /*#__PURE__*/React__default.createElement(Backdrop, {
|
||||
onPress: ctx._onBackdropPress,
|
||||
+ accessibilityLabel: this.props.closeButtonLabel,
|
||||
style: backdropStyles,
|
||||
ref: ctx.onBackdropRef
|
||||
}), ctx._makeOptions());
|
||||
@@ -2090,6 +2094,7 @@
|
||||
}), /*#__PURE__*/React__default.createElement(MenuPlaceholder, {
|
||||
ctx: this,
|
||||
backdropStyles: customStyles.backdrop,
|
||||
+ closeButtonLabel: this.props.closeButtonLabel,
|
||||
ref: this._onPlaceholderRef
|
||||
}))));
|
||||
}
|
||||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index 1db1e643a915e4bfb715e33354678ec1be219f50..007157e366d1935368bdd8eff5e7a0773e183d0f 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -18,6 +18,7 @@ declare module "react-native-popup-menu" {
|
||||
menuProviderWrapper?: StyleProp<ViewStyle>;
|
||||
backdrop?: StyleProp<ViewStyle>;
|
||||
};
|
||||
+ closeButtonLabel: string;
|
||||
backHandler?: boolean | Function;
|
||||
skipInstanceCheck?: boolean;
|
||||
children: React.ReactNode;
|
77
Assets/JoplinLetterBlue.svg
Normal file
77
Assets/JoplinLetterBlue.svg
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 682.66669 682.66669"
|
||||
height="682.66669"
|
||||
width="682.66669"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="JoplinLetterBlue.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview13"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77490232"
|
||||
inkscape:cx="366.49781"
|
||||
inkscape:cy="360.69062"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="708"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs6">
|
||||
<linearGradient
|
||||
id="linearGradient26"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
x1="0">
|
||||
<stop
|
||||
id="stop22"
|
||||
offset="0"
|
||||
style="stop-opacity:1;stop-color:#004caf" />
|
||||
<stop
|
||||
id="stop24"
|
||||
offset="1"
|
||||
style="stop-opacity:1;stop-color:#1f95f8" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath829"><path
|
||||
id="path831"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.999997"
|
||||
d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.052 c 0.48,-16.84 -0.1898,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5003,-4.73 -0.7902,-7.09 -1.0998,-9.53 -2.3199,-19.01 -4.17,-28.29 -1.0098,-5.29 -2.4399,-10.44 -3.7098,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.5802,-40.27 -32.4702,-77.62 -59.9802,-110.5 -1.0098,-1.17 -2.2599,-2.25 -3.2598,-3.41 -8.3901,-9.72 -17.2002,-19.19 -26.9502,-28.06 -9.84,-8.95 -20.2599,-17.27 -31.2099,-25 -77.8401,-55.14 -182.61,-79.4 -299.67,-68.2 -149.2599,14.03 -297.3399,81.72 -417.03,190.62 -119.6701,108.89 -194.08,243.62 -209.4799,379.41 -13.8501,121.48 22.5498,228.38 102.42,301.05 0.21,0.1598 0.3997,0.3098 0.5602,0.48 3.09,2.77 6.4901,5.2 9.6701,7.87 57.16,47.89 131.6701,76.91 216.7,84.91 0.96,0.09 1.8801,0.24 2.79,0.3203 8.9499,0.79 18.0699,1.15 27.27,1.49 4.8099,0.1598 9.5601,0.5003 14.4399,0.54 1.62,0.023 3.1602,0.1898 4.7802,0.1898 2.8998,0 5.91,-0.3803 8.8098,-0.42 13.4001,-0.21 26.9001,-0.7601 40.6701,-1.9401 1.74,-0.1402 3.3999,-0.08 5.19,-0.24 1.2699,-0.1297 2.5299,-0.4102 3.8001,-0.54 78,-7.82 155.2299,-31.11 228.5199,-66.3999 1.53,-0.068 3.3,-0.54 5.5099,-1.7601 22.34,-12.3399 26.6201,0.9 27.2801,9.6501 v 382.2399 282.8201 c 0,19.05 -13.2501,35.8999 -31.83,39.99 -394.7601,86.88 -782.08,-3.5501 -1055.38,-252.3401 -238.7499,-217.1799 -354.24,-530.5799 -316.8201,-859.7899 33.39,-293.23 183.9102,-574.94 423.88,-793.33 233.8901,-212.79003 531.69,-345.86006 838.8801,-374.80106 42.33,-3.918 84.8601,-5.93797 126.36,-5.93797 293.3799,0 565.6099,100.59802 766.54,283.37903 190.3401,173.3 304.35,411.27 321.0799,670.16 l 1.55,1697.91 h 0.1703 v 453.97 h 0.06 v 7.92 c 1.72,80.1199 67.05,144.58 147.61,144.58 h 190.77 c 14.8599,0 26.9199,12.05 26.9199,26.9199 v 441.84 c 0,13.13 -10.6299,23.77 -23.7799,23.77" /></clipPath></defs>
|
||||
<g
|
||||
id="g14"
|
||||
transform="matrix(0.13333333,0,0,-0.13333333,0,682.66667)"
|
||||
mask="none"
|
||||
clip-path="url(#clipPath829)">
|
||||
<g
|
||||
clip-path="url(#clipPath20)"
|
||||
id="g16">
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
BIN
Assets/WebsiteAssets/images/ocr/view_ocr_text.png
Normal file
BIN
Assets/WebsiteAssets/images/ocr/view_ocr_text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/TiktokRise.jpg
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/TiktokRise.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
@@ -80,7 +80,7 @@ async function setupDownloadPage() {
|
||||
|
||||
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
|
||||
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
|
||||
if (href.endsWith('arm64.DMG')) downloadLinks['macOsM1'] = href;
|
||||
if (href.indexOf('arm64.DMG') > 0) downloadLinks['macOsM1'] = href;
|
||||
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
|
||||
});
|
||||
|
||||
@@ -98,6 +98,8 @@ async function setupDownloadPage() {
|
||||
} else {
|
||||
const os = await getOs();
|
||||
|
||||
console.info('Found OS: ' + os);
|
||||
|
||||
if (os === 'macOsUndefined') {
|
||||
// If we don't know which macOS version it is, we let the user choose.
|
||||
$('.main-content .intro').html('<p class="macos-m1-info">The macOS release is available for Intel processors or for Apple Silicon (M1) processors. Please select your version:</p>');
|
||||
|
@@ -398,7 +398,7 @@
|
||||
|
||||
<div class="text-center sponsors-org">
|
||||
{{#sponsors.orgs}}
|
||||
<a class="sponsor-org-item" href="{{url}}"><img title="{{title}}" src="{{imageBaseUrl}}/sponsors/{{imageName}}"></a>
|
||||
<a class="sponsor-org-item" href="{{url}}"><img alt="{{alt}}" title="{{title}}" src="{{imageBaseUrl}}/sponsors/{{imageName}}"></a>
|
||||
{{/sponsors.orgs}}
|
||||
</div>
|
||||
|
||||
|
@@ -7,6 +7,9 @@ FROM node:18 AS builder
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
python3 tini \
|
||||
# needed for node-canvas for ARM32 platform.
|
||||
# See also https://github.com/Automattic/node-canvas/wiki/Installation:-Ubuntu-and-other-Debian-based-systems
|
||||
libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Enables Yarn
|
||||
@@ -47,9 +50,9 @@ RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
|
||||
# Note that `yarn install` ignores `NODE_ENV=production` and will install dev
|
||||
# dependencies too, but this is fine because we need them to build the app.
|
||||
|
||||
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
|
||||
&& yarn cache clean \
|
||||
&& rm -rf .yarn/berry
|
||||
RUN --mount=type=cache,target=/build/.yarn/cache --mount=type=cache,target=/build/.yarn/berry/cache\
|
||||
BUILD_SEQUENCIAL=1 yarn config set cacheFolder /build/.yarn/cache \
|
||||
&& yarn install --inline-builds
|
||||
|
||||
# =============================================================================
|
||||
# Final stage - we copy only the relevant files from the build stage and start
|
||||
@@ -81,10 +84,11 @@ CMD ["yarn", "start-prod"]
|
||||
ARG BUILD_DATE
|
||||
ARG REVISION
|
||||
ARG VERSION
|
||||
ARG SOURCE
|
||||
LABEL org.opencontainers.image.created="$BUILD_DATE" \
|
||||
org.opencontainers.image.title="Joplin Server" \
|
||||
org.opencontainers.image.description="Docker image for Joplin Server" \
|
||||
org.opencontainers.image.url="https://joplinapp.org/" \
|
||||
org.opencontainers.image.revision="$REVISION" \
|
||||
org.opencontainers.image.source="https://github.com/laurent22/joplin.git" \
|
||||
org.opencontainers.image.version="${VERSION}"
|
||||
org.opencontainers.image.source="$SOURCE" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
|
@@ -67,10 +67,23 @@ showHelp() {
|
||||
fi
|
||||
}
|
||||
|
||||
#-----------------------------------------------------
|
||||
# Setup Download Helper: DL
|
||||
#-----------------------------------------------------
|
||||
if [[ `command -v wget2` ]]; then
|
||||
DL='wget2 -qO'
|
||||
elif [[ `command -v wget` ]]; then
|
||||
DL='wget -qO'
|
||||
elif [[ `command -v curl` ]]; then
|
||||
DL='curl -sLo'
|
||||
else
|
||||
print "${COLOR_RED}Error: wget2, wget, and curl not found. Please install one of these tools.${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#-----------------------------------------------------
|
||||
# PARSE ARGUMENTS
|
||||
#-----------------------------------------------------
|
||||
|
||||
optspec=":h-:"
|
||||
while getopts "${optspec}" OPT; do
|
||||
[ "${OPT}" = " " ] && continue
|
||||
@@ -140,9 +153,9 @@ fi
|
||||
|
||||
# Get the latest version to download
|
||||
if [[ "$INCLUDE_PRE_RELEASE" == true ]]; then
|
||||
RELEASE_VERSION=$(wget -qO - "https://api.github.com/repos/laurent22/joplin/releases" | grep -Po '"tag_name": ?"v\K.*?(?=")' | sort -rV | head -1)
|
||||
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases" | grep -Po '"tag_name": ?"v\K.*?(?=")' | sort -rV | head -1)
|
||||
else
|
||||
RELEASE_VERSION=$(wget -qO - "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": ?"v\K.*?(?=")')
|
||||
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": ?"v\K.*?(?=")')
|
||||
fi
|
||||
|
||||
# Check if it's in the latest version
|
||||
@@ -163,8 +176,8 @@ fi
|
||||
#-----------------------------------------------------
|
||||
print 'Downloading Joplin...'
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
|
||||
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
$DL "${TEMP_DIR}/Joplin.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
|
||||
$DL "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
|
||||
#-----------------------------------------------------
|
||||
print 'Installing Joplin...'
|
||||
@@ -287,7 +300,7 @@ echo "$RELEASE_VERSION" > "${INSTALL_DIR}/VERSION"
|
||||
|
||||
#-----------------------------------------------------
|
||||
if [[ "$SHOW_CHANGELOG" == true ]]; then
|
||||
NOTES=$(wget -qO - https://api.github.com/repos/laurent22/joplin/releases/latest | grep -Po '"body": "\K.*(?=")')
|
||||
NOTES=$($DL - https://api.github.com/repos/laurent22/joplin/releases/latest | grep -Po '"body": "\K.*(?=")')
|
||||
print "${COLOR_BLUE}Changelog:${COLOR_RESET}\n${NOTES}"
|
||||
fi
|
||||
|
||||
|
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://ca.edubirdie.com/"><img title="Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!" width="256" src="https://joplinapp.org/images/sponsors/Edubirdie.png" alt="EduBirdie"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="web design agency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://www.reddit.com/r/tiktokRise/"><img title="Tiktok Rise" width="256" src="https://joplinapp.org/images/sponsors/TiktokRise.jpg" alt="Tiktok Rise"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
@@ -42,7 +42,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
|
||||
| | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) | | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
# Community
|
||||
@@ -50,9 +50,10 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
Name | Description
|
||||
--- | ---
|
||||
[Support Forum](https://discourse.joplinapp.org/) | This is the main place for general discussion about Joplin, user support, software development questions, and to discuss new features. Also where the latest beta versions are released and discussed.
|
||||
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
|
||||
[Bluesky feed](https://bsky.app/profile/joplinapp.bsky.social) | Follow us on Bluesky
|
||||
[Mastodon feed](https://mastodon.social/@joplinapp) | Follow us on Mastodon
|
||||
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
|
||||
[YouTube](https://www.youtube.com/@joplinapp) | Discover information and tutorials on how to use the apps
|
||||
[Discord server](https://discord.gg/VSj7AFHvpq) | Our chat server
|
||||
[LinkedIn](https://www.linkedin.com/company/joplin) | Our LinkedIn page
|
||||
[Lemmy Community](https://sopuli.xyz/c/joplinapp) | Also a good place to get help
|
||||
|
30
SECURITY.md
30
SECURITY.md
@@ -10,6 +10,36 @@ Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/
|
||||
|
||||
For general opinions on what makes an app more or less secure, please use the forum.
|
||||
|
||||
## Areas outside Joplin's Threat Model
|
||||
|
||||
Note: we're mostly linking to Chrome's documentation since our reasoning for these exclusions is the same.
|
||||
|
||||
### Denial of Service (DoS)
|
||||
|
||||
[Reference](https://chromium.googlesource.com/chromium/src.git/+/master/docs/security/faq.md#are-denial-of-service-issues-considered-security-bugs)
|
||||
|
||||
### Physically-local attacks
|
||||
|
||||
[Reference](https://chromium.googlesource.com/chromium/src.git/+/master/docs/security/faq.md#why-arent-physically_local-attacks-in-chromes-threat-model)
|
||||
|
||||
### Compromised/infected machines
|
||||
|
||||
[Reference](https://chromium.googlesource.com/chromium/src.git/+/master/docs/security/faq.md#why-arent-compromised_infected-machines-in-chromes-threat-model)
|
||||
|
||||
### Is opening a file on the local machine a security vulnerability?
|
||||
|
||||
No - users are allowed to link to files on their local computer. This was a feature that was implemented by popular request. There are measures in place to mitigate security risks such as a dialog to confirm whether a file with an unknown file extension should be opened.
|
||||
|
||||
### Is DLL sideloading a security vulnerability?
|
||||
|
||||
No. This is an Electron issue and not one they will fix: https://github.com/electron/electron/issues/28384
|
||||
|
||||
See also [Physically-local attacks](https://chromium.googlesource.com/chromium/src.git/+/master/docs/security/faq.md#why-arent-physically_local-attacks-in-chromes-threat-model)
|
||||
|
||||
### Is local data not being encrypted a security vulnerability?
|
||||
|
||||
No, but you should use disk encryption. See also [Physically-local attacks](https://chromium.googlesource.com/chromium/src.git/+/master/docs/security/faq.md#why-arent-physically_local-attacks-in-chromes-threat-model)
|
||||
|
||||
## Bounty
|
||||
|
||||
We **do not** offer a bounty for discovering vulnerabilities, please do not ask. We can however credit you and link to your website in the changelog and release announcement.
|
||||
|
@@ -33,6 +33,7 @@
|
||||
"/packages/app-desktop/build/",
|
||||
"/packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts",
|
||||
"/packages/app-desktop/vendor/",
|
||||
"/packages/app-mobile/android/vendor/",
|
||||
"/packages/app-mobile/ios/Pods/",
|
||||
"/packages/app-mobile/lib/rnInjectedJs",
|
||||
"/packages/app-mobile/pluginAssets",
|
||||
|
@@ -25,7 +25,8 @@
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "latest",
|
||||
"git": "latest",
|
||||
"giflib": "latest",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
@@ -16,7 +16,7 @@ services:
|
||||
- POSTGRES_DATABASE=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=localhost
|
||||
- POSTGRES_HOST=db
|
||||
db:
|
||||
image: postgres:16
|
||||
ports:
|
||||
|
1
fastlane/metadata/android/uk/short_description.txt
Normal file
1
fastlane/metadata/android/uk/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Додаток для заміток і завдань із синхронізацією між Linux, macOS, Windows і мобільними пристроями
|
@@ -108,13 +108,14 @@
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"@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",
|
||||
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
|
||||
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
|
||||
"app-builder-lib@26.0.0-alpha.7": "patch:app-builder-lib@npm%3A26.0.0-alpha.7#./.yarn/patches/app-builder-lib-npm-26.0.0-alpha.7-e1b3dca119.patch",
|
||||
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
|
||||
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch"
|
||||
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
|
||||
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
|
||||
"react-native-popup-menu@0.16.1": "patch:react-native-popup-menu@npm%3A0.16.1#./.yarn/patches/react-native-popup-menu-npm-0.16.1-28fd66ecb5.patch"
|
||||
}
|
||||
}
|
||||
|
@@ -72,7 +72,7 @@
|
||||
"@joplin/tools": "~3.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.19.64",
|
||||
"@types/node": "18.19.67",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
BIN
packages/app-cli/tests/ocr_samples/low_confidence_testing.png
Normal file
BIN
packages/app-cli/tests/ocr_samples/low_confidence_testing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 384 KiB |
BIN
packages/app-cli/tests/support/onenote/corrupted_attachment.zip
Normal file
BIN
packages/app-cli/tests/support/onenote/corrupted_attachment.zip
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,12 @@
|
||||
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/utils/Logger';
|
||||
import { PluginMessage } from './services/plugins/PluginRunner';
|
||||
import AutoUpdaterService, { defaultUpdateInterval, initialUpdateStartup } from './services/autoUpdater/AutoUpdaterService';
|
||||
import type ShimType from '@joplin/lib/shim';
|
||||
const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
|
||||
import { FileLocker } from '@joplin/utils/fs';
|
||||
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App } from 'electron';
|
||||
import bridge from './bridge';
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
@@ -19,6 +20,9 @@ import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProt
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
import { resolve } from 'path';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import { msleep, Second } from '@joplin/utils/time';
|
||||
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
|
||||
import getAppName from '@joplin/lib/getAppName';
|
||||
|
||||
interface RendererProcessQuitReply {
|
||||
canClose: boolean;
|
||||
@@ -34,13 +38,21 @@ interface SecondaryWindowData {
|
||||
electronId: number;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
env: string;
|
||||
profilePath: string|null;
|
||||
isDebugMode: boolean;
|
||||
isEndToEndTesting: boolean;
|
||||
initialCallbackUrl: string;
|
||||
}
|
||||
|
||||
export default class ElectronAppWrapper {
|
||||
private logger_: Logger = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private electronApp_: any;
|
||||
private electronApp_: App;
|
||||
private env_: string;
|
||||
private isDebugMode_: boolean;
|
||||
private profilePath_: string;
|
||||
private isEndToEndTesting_: boolean;
|
||||
|
||||
private win_: BrowserWindow = null;
|
||||
private mainWindowHidden_ = true;
|
||||
@@ -58,13 +70,29 @@ export default class ElectronAppWrapper {
|
||||
private customProtocolHandler_: CustomProtocolHandler = null;
|
||||
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) {
|
||||
private profileLocker_: FileLocker|null = null;
|
||||
private ipcServer_: IpcServer|null = null;
|
||||
private ipcStartPort_ = 2658;
|
||||
|
||||
private ipcLogger_: Logger;
|
||||
|
||||
public constructor(electronApp: App, { env, profilePath, isDebugMode, initialCallbackUrl, isEndToEndTesting }: Options) {
|
||||
this.electronApp_ = electronApp;
|
||||
this.env_ = env;
|
||||
this.isDebugMode_ = isDebugMode;
|
||||
this.profilePath_ = profilePath;
|
||||
this.initialCallbackUrl_ = initialCallbackUrl;
|
||||
this.isEndToEndTesting_ = isEndToEndTesting;
|
||||
|
||||
this.profileLocker_ = new FileLocker(`${this.profilePath_}/lock`);
|
||||
|
||||
// Note: in certain contexts `this.logger_` doesn't seem to be available, especially for IPC
|
||||
// calls, either because it hasn't been set or other issue. So we set one here specifically
|
||||
// for this.
|
||||
this.ipcLogger_ = new Logger();
|
||||
this.ipcLogger_.addTarget(TargetType.File, {
|
||||
path: `${profilePath}/log-cross-app-ipc.txt`,
|
||||
});
|
||||
}
|
||||
|
||||
public electronApp() {
|
||||
@@ -262,15 +290,25 @@ export default class ElectronAppWrapper {
|
||||
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
|
||||
// to this line though.
|
||||
if (debugEarlyBugs) {
|
||||
setTimeout(() => {
|
||||
// Since a recent release of Electron (v34?), calling openDevTools() here does nothing
|
||||
// if a plugin devtool window is already opened. Maybe because they do a check on
|
||||
// `isDevToolsOpened` which indeed returns `true` (but shouldn't since it's for a
|
||||
// different window). However, if you open the dev tools twice from the Help menu it
|
||||
// works. So instead we do that here and call openDevTool() three times.
|
||||
let openDevToolCount = 0;
|
||||
const openDevToolInterval = setInterval(() => {
|
||||
try {
|
||||
this.win_.webContents.openDevTools();
|
||||
openDevToolCount++;
|
||||
if (openDevToolCount >= 3) {
|
||||
clearInterval(openDevToolInterval);
|
||||
}
|
||||
} catch (error) {
|
||||
// This will throw an exception "Object has been destroyed" if the app is closed
|
||||
// in less that the timeout interval. It can be ignored.
|
||||
// This will throw an exception "Object has been destroyed" if the app is closed
|
||||
// in less that the timeout interval. It can be ignored.
|
||||
console.warn('Error opening dev tools', error);
|
||||
}
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const addWindowEventHandlers = (webContents: WebContents) => {
|
||||
@@ -400,7 +438,7 @@ export default class ElectronAppWrapper {
|
||||
if (message.target === 'plugin') {
|
||||
const win = this.pluginWindows_[message.pluginId];
|
||||
if (!win) {
|
||||
this.logger().error(`Trying to send IPC message to non-existing plugin window: ${message.pluginId}`);
|
||||
this.ipcLogger_.error(`Trying to send IPC message to non-existing plugin window: ${message.pluginId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -455,12 +493,24 @@ export default class ElectronAppWrapper {
|
||||
});
|
||||
}
|
||||
|
||||
public quit() {
|
||||
private onExit() {
|
||||
this.stopPeriodicUpdateCheck();
|
||||
this.profileLocker_.unlockSync();
|
||||
|
||||
// Probably doesn't matter if the server is not closed cleanly? Thus the lack of `await`
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Needed here because onExit() is not async
|
||||
void stopServer(this.ipcServer_).catch(_error => {
|
||||
// Ignore it since we're stopping, and to prevent unnecessary messages.
|
||||
});
|
||||
}
|
||||
|
||||
public quit() {
|
||||
this.onExit();
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
|
||||
public exit(errorCode = 0) {
|
||||
this.onExit();
|
||||
this.electronApp_.exit(errorCode);
|
||||
}
|
||||
|
||||
@@ -526,20 +576,32 @@ export default class ElectronAppWrapper {
|
||||
this.tray_ = null;
|
||||
}
|
||||
|
||||
public ensureSingleInstance() {
|
||||
if (this.env_ === 'dev') return false;
|
||||
public async sendCrossAppIpcMessage(message: Message, port: number|null = null, options: SendMessageOptions = null) {
|
||||
this.ipcLogger_.info('Sending message:', message);
|
||||
|
||||
const gotTheLock = this.electronApp_.requestSingleInstanceLock();
|
||||
if (port === null) port = this.ipcStartPort_;
|
||||
|
||||
if (!gotTheLock) {
|
||||
// Another instance is already running - exit
|
||||
this.quit();
|
||||
return true;
|
||||
return await sendMessage(port, {
|
||||
...message,
|
||||
sourcePort: this.ipcServer_.port,
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
public async ensureSingleInstance() {
|
||||
// When end-to-end testing, multiple instances of Joplin are intentionally created at the same time,
|
||||
// or very close to one another. The single instance handling logic can interfere with this, so disable it.
|
||||
if (this.isEndToEndTesting_) return false;
|
||||
|
||||
interface OnSecondInstanceMessageData {
|
||||
profilePath: string;
|
||||
argv: string[];
|
||||
}
|
||||
|
||||
// Someone tried to open a second instance - focus our window instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.electronApp_.on('second-instance', (_e: any, argv: string[]) => {
|
||||
const activateWindow = (argv: string[]) => {
|
||||
const win = this.mainWindow();
|
||||
if (!win) return;
|
||||
if (win.isMinimized()) win.restore();
|
||||
@@ -552,9 +614,96 @@ export default class ElectronAppWrapper {
|
||||
void this.openCallbackUrl(url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const messageHandlers: Record<string, IpcMessageHandler> = {
|
||||
'onSecondInstance': async (message) => {
|
||||
const data = message.data as OnSecondInstanceMessageData;
|
||||
if (data.profilePath === this.profilePath_) activateWindow(data.argv);
|
||||
},
|
||||
|
||||
'restartAltInstance': async (message) => {
|
||||
if (bridge().altInstanceId()) return false;
|
||||
|
||||
// We do this in a timeout after a short interval because we need this call to
|
||||
// return the response immediately, so that the caller can call `quit()`
|
||||
setTimeout(async () => {
|
||||
const maxWait = 10000;
|
||||
const interval = 300;
|
||||
const loopCount = Math.ceil(maxWait / interval);
|
||||
let callingAppGone = false;
|
||||
|
||||
for (let i = 0; i < loopCount; i++) {
|
||||
const response = await this.sendCrossAppIpcMessage({
|
||||
action: 'ping',
|
||||
data: null,
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
}, message.sourcePort, {
|
||||
sendToSpecificPortOnly: true,
|
||||
});
|
||||
|
||||
if (!response.length) {
|
||||
callingAppGone = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await msleep(interval);
|
||||
}
|
||||
|
||||
if (callingAppGone) {
|
||||
// Wait a bit more because even if the app is not responding, the process
|
||||
// might still be there for a short while.
|
||||
await msleep(1000);
|
||||
this.ipcLogger_.warn('restartAltInstance: App is gone - restarting it');
|
||||
void bridge().launchNewAppInstance(this.env());
|
||||
} else {
|
||||
this.ipcLogger_.warn('restartAltInstance: Could not restart calling app because it was still open');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
'ping': async (_message) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const defaultProfileDir = determineBaseAppDirs('', getAppName(true, this.env() === 'dev'), '').rootProfileDir;
|
||||
const secretKeyFilePath = `${defaultProfileDir}/ipc_secret_key.txt`;
|
||||
|
||||
this.ipcLogger_.info('Starting server using secret key:', secretKeyFilePath);
|
||||
|
||||
this.ipcServer_ = await startServer(this.ipcStartPort_, secretKeyFilePath, async (message) => {
|
||||
if (messageHandlers[message.action]) {
|
||||
this.ipcLogger_.info('Got message:', message);
|
||||
return messageHandlers[message.action](message);
|
||||
}
|
||||
|
||||
throw newHttpError(404);
|
||||
}, {
|
||||
logger: this.ipcLogger_,
|
||||
});
|
||||
|
||||
return false;
|
||||
// First check that no other app is running from that profile folder
|
||||
const gotAppLock = await this.profileLocker_.lock();
|
||||
if (gotAppLock) return false;
|
||||
|
||||
const message: Message = {
|
||||
action: 'onSecondInstance',
|
||||
data: {
|
||||
senderPort: this.ipcServer_.port,
|
||||
profilePath: this.profilePath_,
|
||||
argv: process.argv,
|
||||
},
|
||||
secretKey: this.ipcServer_.secretKey,
|
||||
};
|
||||
|
||||
await this.sendCrossAppIpcMessage(message);
|
||||
|
||||
this.quit();
|
||||
if (this.env() === 'dev') console.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
public initializeCustomProtocolHandler(logger: LoggerWrapper) {
|
||||
@@ -596,7 +745,7 @@ export default class ElectronAppWrapper {
|
||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||
await this.waitForElectronAppReady();
|
||||
|
||||
const alreadyRunning = this.ensureSingleInstance();
|
||||
const alreadyRunning = await this.ensureSingleInstance();
|
||||
if (alreadyRunning) return;
|
||||
|
||||
this.createWindow();
|
||||
|
@@ -617,10 +617,11 @@ class Application extends BaseApplication {
|
||||
clipperLogger.addTarget(TargetType.Console);
|
||||
|
||||
ClipperServer.instance().initialize(actionApi);
|
||||
ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
ClipperServer.instance().setDispatch(this.store().dispatch);
|
||||
|
||||
if (Setting.value('clipperServer.autoStart')) {
|
||||
if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import isSafeToOpen from './utils/isSafeToOpen';
|
||||
import { closeSync, openSync, readSync, statSync } from 'fs';
|
||||
import { KB } from '@joplin/utils/bytes';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
|
||||
interface LastSelectedPath {
|
||||
file: string;
|
||||
@@ -43,16 +44,18 @@ export class Bridge {
|
||||
private appName_: string;
|
||||
private appId_: string;
|
||||
private logFilePath_ = '';
|
||||
private altInstanceId_ = '';
|
||||
|
||||
private extraAllowedExtensions_: string[] = [];
|
||||
private onAllowedExtensionsChangeListener_: OnAllowedExtensionsChange = ()=>{};
|
||||
|
||||
public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) {
|
||||
public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) {
|
||||
this.electronWrapper_ = electronWrapper;
|
||||
this.appId_ = appId;
|
||||
this.appName_ = appName;
|
||||
this.rootProfileDir_ = rootProfileDir;
|
||||
this.autoUploadCrashDumps_ = autoUploadCrashDumps;
|
||||
this.altInstanceId_ = altInstanceId;
|
||||
this.lastSelectedPaths_ = {
|
||||
file: null,
|
||||
directory: null,
|
||||
@@ -118,6 +121,8 @@ export class Bridge {
|
||||
return event;
|
||||
}
|
||||
},
|
||||
|
||||
integrations: [Sentry.electronMinidumpIntegration()],
|
||||
};
|
||||
|
||||
if (this.autoUploadCrashDumps_) options.dsn = 'https://cceec550871b1e8a10fee4c7a28d5cf2@o4506576757522432.ingest.sentry.io/4506594281783296';
|
||||
@@ -216,6 +221,10 @@ export class Bridge {
|
||||
return this.electronApp().electronApp().getLocale();
|
||||
};
|
||||
|
||||
public altInstanceId() {
|
||||
return this.altInstanceId_;
|
||||
}
|
||||
|
||||
// Applies to electron-context-menu@3:
|
||||
//
|
||||
// For now we have to disable spell checking in non-editor text
|
||||
@@ -489,7 +498,38 @@ export class Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
public restart(linuxSafeRestart = true) {
|
||||
public appLaunchCommand(env: string, altInstanceId = '') {
|
||||
const altInstanceArgs = altInstanceId ? ['--alt-instance-id', altInstanceId] : [];
|
||||
|
||||
if (env === 'dev') {
|
||||
// This is convenient to quickly test on dev, but the path needs to be adjusted
|
||||
// depending on how things are setup.
|
||||
|
||||
return {
|
||||
execPath: `${homedir()}/.npm-global/bin/electron`,
|
||||
args: [
|
||||
`${homedir()}/src/joplin/packages/app-desktop`,
|
||||
'--env', 'dev',
|
||||
'--log-level', 'debug',
|
||||
'--open-dev-tools',
|
||||
'--no-welcome',
|
||||
].concat(altInstanceArgs),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
execPath: bridge().electronApp().electronApp().getPath('exe'),
|
||||
args: [].concat(altInstanceArgs),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async launchNewAppInstance(env: string) {
|
||||
const cmd = this.appLaunchCommand(env, 'alt1');
|
||||
|
||||
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
|
||||
}
|
||||
|
||||
public async restart() {
|
||||
// Note that in this case we are not sending the "appClose" event
|
||||
// to notify services and component that the app is about to close
|
||||
// but for the current use-case it's not really needed.
|
||||
@@ -500,13 +540,39 @@ export class Bridge {
|
||||
execPath: process.env.PORTABLE_EXECUTABLE_FILE,
|
||||
};
|
||||
app.relaunch(options);
|
||||
} else if (shim.isLinux() && linuxSafeRestart) {
|
||||
this.showInfoMessageBox(_('The app is now going to close. Please relaunch it to complete the process.'));
|
||||
} else if (this.altInstanceId_) {
|
||||
// Couldn't get it to work using relaunch() - it would just "close" the app, but it
|
||||
// would still be open in the tray except unusable. Or maybe it reopens it quickly but
|
||||
// in a broken state. It might be due to the way it is launched from the main instance.
|
||||
// So here we ask the main instance to relaunch this app after a short delay.
|
||||
|
||||
const responses = await this.electronApp().sendCrossAppIpcMessage({
|
||||
action: 'restartAltInstance',
|
||||
data: null,
|
||||
});
|
||||
|
||||
// However is the main instance is not running, we're stuck, so the user needs to
|
||||
// manually restart. `relaunch()` doesn't appear to work even when the main instance is
|
||||
// not running.
|
||||
const r = responses.find(r => !!r.response);
|
||||
|
||||
if (!r || !r.response) {
|
||||
this.showInfoMessageBox(_('The app is now going to close. Please relaunch it to complete the process.'));
|
||||
|
||||
// Note: this should work, but doesn't:
|
||||
|
||||
// const cmd = this.appLaunchCommand(this.env(), this.altInstanceId_);
|
||||
|
||||
// app.relaunch({
|
||||
// execPath: cmd.execPath,
|
||||
// args: cmd.args,
|
||||
// });
|
||||
}
|
||||
} else {
|
||||
app.relaunch();
|
||||
}
|
||||
|
||||
app.exit();
|
||||
this.electronApp().exit();
|
||||
}
|
||||
|
||||
public createImageFromPath(path: string) {
|
||||
@@ -532,9 +598,9 @@ export class Bridge {
|
||||
|
||||
let bridge_: Bridge = null;
|
||||
|
||||
export function initBridge(wrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) {
|
||||
export function initBridge(wrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) {
|
||||
if (bridge_) throw new Error('Bridge already initialized');
|
||||
bridge_ = new Bridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps);
|
||||
bridge_ = new Bridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
|
||||
return bridge_;
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import * as exportDeletionLog from './exportDeletionLog';
|
||||
import * as exportFolders from './exportFolders';
|
||||
import * as exportNotes from './exportNotes';
|
||||
import * as focusElement from './focusElement';
|
||||
import * as newAppInstance from './newAppInstance';
|
||||
import * as openNoteInNewWindow from './openNoteInNewWindow';
|
||||
import * as openProfileDirectory from './openProfileDirectory';
|
||||
import * as replaceMisspelling from './replaceMisspelling';
|
||||
@@ -28,6 +29,7 @@ const index: any[] = [
|
||||
exportFolders,
|
||||
exportNotes,
|
||||
focusElement,
|
||||
newAppInstance,
|
||||
openNoteInNewWindow,
|
||||
openProfileDirectory,
|
||||
replaceMisspelling,
|
||||
|
19
packages/app-desktop/commands/newAppInstance.ts
Normal file
19
packages/app-desktop/commands/newAppInstance.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newAppInstance',
|
||||
label: () => _('New application instance...'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await bridge().launchNewAppInstance(Setting.value('env'));
|
||||
},
|
||||
|
||||
enabledCondition: '!isAltInstance',
|
||||
};
|
||||
};
|
@@ -24,6 +24,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
private enableClipperServer_click() {
|
||||
if (!ClipperServer.instance().enabled()) return;
|
||||
Setting.setValue('clipperServer.autoStart', true);
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
@@ -70,6 +71,8 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
|
||||
const webClipperStatusComps = [];
|
||||
|
||||
const clipperEnabled = ClipperServer.instance().enabled();
|
||||
|
||||
if (this.props.clipperServerAutoStart) {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_1" style={theme.textStyle}>
|
||||
@@ -95,13 +98,22 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
if (!clipperEnabled) {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_4" style={theme.textStyle}>
|
||||
{_('The web clipper service cannot be enabled in this instance of Joplin.')}
|
||||
</p>,
|
||||
);
|
||||
} else {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_4" style={theme.textStyle}>
|
||||
{_('The web clipper service is not enabled.')}
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_4" style={theme.textStyle}>
|
||||
{_('The web clipper service is not enabled.')}
|
||||
</p>,
|
||||
);
|
||||
webClipperStatusComps.push(
|
||||
<button key="enable_button" style={buttonStyle} onClick={this.enableClipperServer_click}>
|
||||
<button key="enable_button" style={buttonStyle} onClick={this.enableClipperServer_click} disabled={!clipperEnabled}>
|
||||
{_('Enable Web Clipper Service')}
|
||||
</button>,
|
||||
);
|
||||
|
@@ -63,23 +63,70 @@ const Dialog: React.FC<Props> = props => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
// We keep track of the mouse events to allow the action to be cancellable on the mouseup
|
||||
// If dialogElement is the source of the mouse event it means
|
||||
// that the user clicked in the dimmed background and not in the content of the dialog
|
||||
const useClickedOutsideContent = (dialogElement: HTMLDialogElement|null) => {
|
||||
const mouseDownOutsideContent = useRef(false);
|
||||
mouseDownOutsideContent.current = false;
|
||||
const [clickedOutsideContent, setClickedOutsideContent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogElement) return () => {};
|
||||
|
||||
const mouseDownListener = (event: MouseEvent) => {
|
||||
if (event.target === dialogElement) {
|
||||
mouseDownOutsideContent.current = true;
|
||||
} else {
|
||||
mouseDownOutsideContent.current = false;
|
||||
}
|
||||
};
|
||||
const mouseUpListener = (event: MouseEvent) => {
|
||||
if (!mouseDownOutsideContent.current) return;
|
||||
if (mouseDownOutsideContent.current && event.target === dialogElement) {
|
||||
setClickedOutsideContent(true);
|
||||
mouseDownOutsideContent.current = false;
|
||||
} else {
|
||||
setClickedOutsideContent(false);
|
||||
mouseDownOutsideContent.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
dialogElement.addEventListener('mousedown', mouseDownListener);
|
||||
dialogElement.addEventListener('mouseup', mouseUpListener);
|
||||
|
||||
return () => {
|
||||
dialogElement.removeEventListener('mousedown', mouseDownListener);
|
||||
dialogElement.removeEventListener('mouseup', mouseUpListener);
|
||||
};
|
||||
}, [dialogElement]);
|
||||
|
||||
return [clickedOutsideContent, setClickedOutsideContent] as const;
|
||||
};
|
||||
|
||||
const useDialogElement = (containerDocument: Document, onCancel: undefined|OnCancelListener) => {
|
||||
const [dialogElement, setDialogElement] = useState<HTMLDialogElement|null>(null);
|
||||
|
||||
const onCancelRef = useRef(onCancel);
|
||||
onCancelRef.current = onCancel;
|
||||
|
||||
const [clickedOutsideContent, setClickedOutsideContent] = useClickedOutsideContent(dialogElement);
|
||||
|
||||
useEffect(() => {
|
||||
if (clickedOutsideContent) {
|
||||
const onCancel = onCancelRef.current;
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
setClickedOutsideContent(false);
|
||||
}
|
||||
}
|
||||
}, [clickedOutsideContent, setClickedOutsideContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerDocument) return () => {};
|
||||
|
||||
const dialog = containerDocument.createElement('dialog');
|
||||
dialog.addEventListener('click', event => {
|
||||
const onCancel = onCancelRef.current;
|
||||
const isBackgroundClick = event.target === dialog;
|
||||
if (isBackgroundClick && onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
dialog.classList.add('dialog-modal-layer');
|
||||
dialog.addEventListener('cancel', event => {
|
||||
const canCancel = !!onCancelRef.current;
|
||||
|
@@ -83,6 +83,7 @@ interface Props {
|
||||
notesColumns: NoteListColumns;
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
toast: Toast;
|
||||
shouldSwitchToAppleSiliconVersion: boolean;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -478,6 +479,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onDisableSync = () => {
|
||||
Setting.setValue('sync.target', null);
|
||||
};
|
||||
|
||||
const onViewSyncSettingsScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -488,6 +493,11 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onDownloadAppleSiliconVersion = () => {
|
||||
// The website should redirect to the correct version
|
||||
shim.openUrl('https://joplinapp.org/download/');
|
||||
};
|
||||
|
||||
const onRestartAndUpgrade = async () => {
|
||||
Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO);
|
||||
await Setting.saveAll();
|
||||
@@ -570,11 +580,19 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
} else if (this.props.mustUpgradeAppMessage) {
|
||||
msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage);
|
||||
} else if (this.props.shouldSwitchToAppleSiliconVersion) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('You are running the Intel version of Joplin on an Apple Silicon processor. Download the Apple Silicon one for better performance.'),
|
||||
_('Download it now'),
|
||||
onDownloadAppleSiliconVersion,
|
||||
);
|
||||
} else if (this.props.showInvalidJoplinCloudCredential) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Your Joplin Cloud credentials are invalid, please login.'),
|
||||
_('Login to Joplin Cloud.'),
|
||||
onViewJoplinCloudLoginScreen,
|
||||
_('Disable synchronisation'),
|
||||
onDisableSync,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -605,7 +623,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
this.showShareInvitationNotification(props) ||
|
||||
this.props.needApiAuth ||
|
||||
!!this.props.mustUpgradeAppMessage ||
|
||||
props.showInvalidJoplinCloudCredential;
|
||||
props.showInvalidJoplinCloudCredential ||
|
||||
props.shouldSwitchToAppleSiliconVersion;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -785,7 +804,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
dispatch={this.props.dispatch as any}
|
||||
/>
|
||||
<UpdateNotification themeId={this.props.themeId} />
|
||||
<UpdateNotification />
|
||||
<PluginNotification
|
||||
themeId={this.props.themeId}
|
||||
toast={this.props.toast}
|
||||
@@ -833,6 +852,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
toast: state.toast,
|
||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -172,6 +172,7 @@ interface Props {
|
||||
pluginMenus: any[];
|
||||
['spellChecker.enabled']: boolean;
|
||||
['spellChecker.languages']: string[];
|
||||
markdownEditorVisible: boolean;
|
||||
plugins: PluginStates;
|
||||
customCss: string;
|
||||
locale: string;
|
||||
@@ -278,6 +279,7 @@ function useMenuStates(menu: any, props: Props) {
|
||||
props['notes.sortOrder.reverse'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['folders.sortOrder.reverse'],
|
||||
props.markdownEditorVisible,
|
||||
props.tabMovesFocus,
|
||||
props.noteListRendererId,
|
||||
props.showNoteCounts,
|
||||
@@ -479,6 +481,7 @@ function useMenu(props: Props) {
|
||||
menuItemDic.focusElementNoteList,
|
||||
menuItemDic.focusElementNoteTitle,
|
||||
menuItemDic.focusElementNoteBody,
|
||||
menuItemDic.focusElementNoteViewer,
|
||||
menuItemDic.focusElementToolbar,
|
||||
];
|
||||
|
||||
@@ -552,6 +555,7 @@ function useMenu(props: Props) {
|
||||
const newFolderItem = menuItemDic.newFolder;
|
||||
const newSubFolderItem = menuItemDic.newSubFolder;
|
||||
const printItem = menuItemDic.print;
|
||||
const newAppInstance = menuItemDic.newAppInstance;
|
||||
const switchProfileItem = {
|
||||
label: _('Switch profile'),
|
||||
submenu: switchProfileMenuItems,
|
||||
@@ -715,8 +719,11 @@ function useMenu(props: Props) {
|
||||
}, {
|
||||
type: 'separator',
|
||||
},
|
||||
printItem,
|
||||
printItem, {
|
||||
type: 'separator',
|
||||
},
|
||||
switchProfileItem,
|
||||
newAppInstance,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -789,6 +796,7 @@ function useMenu(props: Props) {
|
||||
shim.isMac() ? noItem : menuItemDic.toggleMenuBar,
|
||||
menuItemDic.toggleNoteList,
|
||||
menuItemDic.toggleVisiblePanes,
|
||||
menuItemDic.toggleEditorPlugin,
|
||||
{
|
||||
label: _('Layout button sequence'),
|
||||
submenu: layoutButtonSequenceMenuItems,
|
||||
@@ -999,6 +1007,7 @@ function useMenu(props: Props) {
|
||||
|
||||
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
|
||||
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
|
||||
rootMenus.tools.submenu.push(menuItemDic.linkToNote);
|
||||
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
|
||||
|
||||
for (const view of props.pluginMenuItems) {
|
||||
@@ -1138,7 +1147,7 @@ function MenuBar(props: Props): any {
|
||||
|
||||
|
||||
const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: state.windowId });
|
||||
|
||||
const secondaryWindowFocused = state.windowId !== defaultWindowId;
|
||||
|
||||
@@ -1164,6 +1173,7 @@ const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
pluginMenus: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menu') }, 'menuBar.pluginMenus'),
|
||||
['spellChecker.languages']: state.settings['spellChecker.languages'],
|
||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||
markdownEditorVisible: whenClauseContext.markdownEditorVisible,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customViewerCss,
|
||||
profileConfig: state.profileConfig,
|
||||
|
@@ -383,10 +383,13 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
// Update the editor's value
|
||||
useEffect(() => {
|
||||
if (editorRef.current?.updateBody(props.content)) {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
const updateProps = { noteId: props.noteId };
|
||||
if (editorRef.current?.updateBody(props.content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
}
|
||||
}, [props.content]);
|
||||
}, [props.content, props.noteId]);
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
@@ -394,6 +397,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
<Editor
|
||||
style={styles.editor}
|
||||
initialText={props.content}
|
||||
initialNoteId={props.noteId}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
pluginStates={props.plugins}
|
||||
|
@@ -129,6 +129,14 @@ const useEditorCommands = (props: Props) => {
|
||||
props.webviewRef.current.send('focus');
|
||||
}
|
||||
},
|
||||
'viewer.focus': () => {
|
||||
if (props.visiblePanes.includes('viewer')) {
|
||||
const editorCursorLine = editorRef.current.getCursor().line;
|
||||
props.webviewRef.current.focusLine(editorCursorLine);
|
||||
} else {
|
||||
logger.info('Viewer not focused (not visible).');
|
||||
}
|
||||
},
|
||||
search: () => {
|
||||
return editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
|
@@ -617,6 +617,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
background: none;
|
||||
background-color: ${theme.backgroundColor3} !important;
|
||||
}
|
||||
|
||||
.tox .tox-tbtn,
|
||||
.tox .tox-tbtn button,
|
||||
.tox .tox-split-button,
|
||||
.tox .tox-split-button button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
`));
|
||||
|
||||
return () => {
|
||||
@@ -673,7 +680,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// we create small groups of just one button towards the end.
|
||||
|
||||
const toolbar = [
|
||||
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|',
|
||||
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', '|',
|
||||
'joplinInsert', 'joplinSup', 'joplinSub', 'forecolor', '|',
|
||||
'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|',
|
||||
'bullist', 'numlist', 'joplinChecklist', '|',
|
||||
'h1', 'h2', 'h3', '|',
|
||||
@@ -728,8 +736,28 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
joplinSub: { inline: 'sub', remove: 'all' },
|
||||
joplinSup: { inline: 'sup', remove: 'all' },
|
||||
code: { inline: 'code', remove: 'all', attributes: { spellcheck: 'false' } },
|
||||
forecolor: { inline: 'span', styles: { color: '%value' } },
|
||||
// Foreground color: The remove_similar: true is necessary here for the "remove formatting"
|
||||
// button to work. See https://github.com/tinymce/tinymce/issues/5026.
|
||||
forecolor: { inline: 'span', styles: { color: '%value' }, remove_similar: true },
|
||||
},
|
||||
text_patterns: props.enableTextPatterns ? [
|
||||
// See https://www.tiny.cloud/docs/tinymce/latest/content-behavior-options/#text_patterns
|
||||
// for the default value
|
||||
{ start: '==', end: '==', format: 'joplinHighlight' },
|
||||
{ start: '`', end: '`', format: 'code' },
|
||||
{ start: '*', end: '*', format: 'italic' },
|
||||
{ start: '**', end: '**', format: 'bold' },
|
||||
{ start: '#', format: 'h1' },
|
||||
{ start: '##', format: 'h2' },
|
||||
{ start: '###', format: 'h3' },
|
||||
{ start: '####', format: 'h4' },
|
||||
{ start: '#####', format: 'h5' },
|
||||
{ start: '######', format: 'h6' },
|
||||
{ start: '1.', cmd: 'InsertOrderedList' },
|
||||
{ start: '*', cmd: 'InsertUnorderedList' },
|
||||
{ start: '-', cmd: 'InsertUnorderedList' },
|
||||
] : [],
|
||||
|
||||
setup: (editor: Editor) => {
|
||||
editor.addCommand('joplinAttach', () => {
|
||||
insertResourcesIntoContentRef.current();
|
||||
@@ -1097,6 +1125,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
// Meta+P is bound by default to print by TinyMCE. It can be unbound, but it seems necessary
|
||||
// to do so after the editor loads. Meta+P should be able to trigger Joplin built-in shortcuts.
|
||||
editor.shortcuts.remove('Meta+P');
|
||||
}, [editor]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Handle onChange event
|
||||
// -----------------------------------------------------------------------------------------
|
||||
@@ -1344,7 +1379,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
editor.on(TinyMceEditorEvents.KeyUp, onKeyUp);
|
||||
editor.on(TinyMceEditorEvents.KeyDown, onKeyDown);
|
||||
editor.on(TinyMceEditorEvents.KeyPress, onKeypress);
|
||||
editor.on(TinyMceEditorEvents.Paste, onPaste);
|
||||
// Passing `true` adds the listener to the front of the listener list.
|
||||
// This allows overriding TinyMCE's built-in paste handler with .preventDefault.
|
||||
editor.on(TinyMceEditorEvents.Paste, onPaste, true);
|
||||
editor.on(TinyMceEditorEvents.PasteAsText, onPasteAsText);
|
||||
editor.on(TinyMceEditorEvents.Copy, onCopy);
|
||||
// `compositionend` means that a user has finished entering a Chinese
|
||||
|
@@ -60,14 +60,12 @@ export default function(editor: any) {
|
||||
});
|
||||
}
|
||||
|
||||
const items: string[] = definitions.filter(d => !!d.grouped).map(d => d.name);
|
||||
|
||||
// Additional built-in buttons to show in the formatting sub-menu:
|
||||
items.push('forecolor');
|
||||
|
||||
editor.ui.registry.addGroupToolbarButton('formattingExtras', {
|
||||
icon: 'image-options',
|
||||
tooltip: _('Formatting'),
|
||||
items: items.join(' '),
|
||||
});
|
||||
// Old code to format a group of buttons into a dropdown
|
||||
// const items: string[] = definitions.filter(d => !!d.grouped).map(d => d.name);
|
||||
// items.push('forecolor');
|
||||
// editor.ui.registry.addGroupToolbarButton('formattingExtras', {
|
||||
// icon: 'image-options',
|
||||
// tooltip: _('Formatting'),
|
||||
// items: items.join(' '),
|
||||
// });
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { menuItems } from '../../../utils/contextMenu';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import type { Event as ElectronEvent } from 'electron';
|
||||
import type { Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
|
||||
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { TinyMceEditorEvents } from './types';
|
||||
@@ -17,6 +17,7 @@ import { Editor } from 'tinymce';
|
||||
import { EditDialogControl } from './useEditDialog';
|
||||
import { Dispatch } from 'redux';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import type { MenuItem as MenuItemType } from 'electron';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -137,13 +138,20 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
event.preventDefault();
|
||||
|
||||
const menu = new Menu();
|
||||
const menuItems = [];
|
||||
const menuItems: MenuItemType[] = [];
|
||||
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
|
||||
return specs.map(spec => new MenuItem(spec));
|
||||
};
|
||||
|
||||
menuItems.push(...makeEditableMenuItems(element));
|
||||
menuItems.push(...makeMainMenuItems(element));
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
menuItems.push(...spellCheckerMenuItems);
|
||||
menuItems.push(...menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu));
|
||||
menuItems.push(
|
||||
...toMenuItems(spellCheckerMenuItems),
|
||||
);
|
||||
menuItems.push(
|
||||
...toMenuItems(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)),
|
||||
);
|
||||
|
||||
for (const item of menuItems) {
|
||||
menu.append(item);
|
||||
|
@@ -52,10 +52,8 @@ import Logger from '@joplin/utils/Logger';
|
||||
import usePluginEditorView from './utils/usePluginEditorView';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
|
||||
@@ -72,15 +70,6 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
||||
let editorIdCounter = 0;
|
||||
|
||||
const makeNoteUpdateAction = (shownEditorViewIds: string[]) => {
|
||||
return async () => {
|
||||
for (const viewId of shownEditorViewIds) {
|
||||
const controller = PluginService.instance().viewControllerByViewId(viewId) as WebviewController;
|
||||
if (controller) controller.emitUpdate();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function NoteEditorContent(props: NoteEditorProps) {
|
||||
const [showRevisions, setShowRevisions] = useState(false);
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
@@ -90,7 +79,10 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const titleInputRef = useRef<HTMLInputElement>();
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
const viewUpdateAsyncQueue_ = useRef<AsyncActionQueue>(new AsyncActionQueue(100, IntervalType.Fixed));
|
||||
|
||||
const editorPluginHandler = useMemo(() => {
|
||||
return new EditorPluginHandler(PluginService.instance());
|
||||
}, []);
|
||||
|
||||
const shownEditorViewIds = props['plugins.shownEditorViewIds'];
|
||||
|
||||
@@ -114,25 +106,15 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
useAsyncEffect(async (_event) => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
|
||||
let filterObject: EditorActivationCheckFilterObject = {
|
||||
activatedEditors: [],
|
||||
};
|
||||
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||
if (event.cancelled) return;
|
||||
|
||||
for (const editor of filterObject.activatedEditors) {
|
||||
const controller = PluginService.instance().pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||
controller.setActive(editor.isActive);
|
||||
}
|
||||
}, [effectiveNoteId, props.startupPluginsLoaded]);
|
||||
await editorPluginHandler.emitActivationCheck();
|
||||
}, [effectiveNoteId, editorPluginHandler, props.startupPluginsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
viewUpdateAsyncQueue_.current.push(makeNoteUpdateAction(shownEditorViewIds));
|
||||
}, [effectiveNoteId, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
editorPluginHandler.emitUpdate(shownEditorViewIds);
|
||||
}, [effectiveNoteId, editorPluginHandler, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds);
|
||||
const builtInEditorVisible = !editorPlugin;
|
||||
@@ -469,6 +451,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
searchMarkers: searchMarkers,
|
||||
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
|
||||
keyboardMode: Setting.value('editor.keyboardMode'),
|
||||
enableTextPatterns: Setting.value('editor.enableTextPatterns'),
|
||||
tabMovesFocus: props.tabMovesFocus,
|
||||
locale: Setting.value('locale'),
|
||||
onDrop: onDrop,
|
||||
|
@@ -38,6 +38,7 @@ const incompatiblePluginIds = [
|
||||
'ylc395.noteLinkSystem',
|
||||
'outline',
|
||||
'joplin.plugin.cmoptions',
|
||||
'com.asdibiase.joplin-languagetool',
|
||||
// cSpell:enable
|
||||
];
|
||||
|
||||
|
@@ -0,0 +1,22 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { FocusElementOptions } from '../../../commands/focusElement';
|
||||
import { WindowCommandDependencies } from '../utils/types';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementNoteViewer',
|
||||
label: () => _('Note viewer'),
|
||||
parentLabel: () => _('Focus'),
|
||||
};
|
||||
|
||||
export const runtime = (dependencies: WindowCommandDependencies): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: unknown, options?: FocusElementOptions) => {
|
||||
await dependencies.editorRef.current.execCommand({
|
||||
name: 'viewer.focus',
|
||||
value: options,
|
||||
});
|
||||
},
|
||||
enabledCondition: 'markdownEditorVisible',
|
||||
};
|
||||
};
|
@@ -1,6 +1,7 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as focusElementNoteBody from './focusElementNoteBody';
|
||||
import * as focusElementNoteTitle from './focusElementNoteTitle';
|
||||
import * as focusElementNoteViewer from './focusElementNoteViewer';
|
||||
import * as focusElementToolbar from './focusElementToolbar';
|
||||
import * as pasteAsText from './pasteAsText';
|
||||
import * as showLocalSearch from './showLocalSearch';
|
||||
@@ -9,6 +10,7 @@ import * as showRevisions from './showRevisions';
|
||||
const index: any[] = [
|
||||
focusElementNoteBody,
|
||||
focusElementNoteTitle,
|
||||
focusElementNoteViewer,
|
||||
focusElementToolbar,
|
||||
pasteAsText,
|
||||
showLocalSearch,
|
||||
|
@@ -163,6 +163,9 @@ const declarations: CommandDeclaration[] = [
|
||||
{
|
||||
name: 'editor.execCommand',
|
||||
},
|
||||
{
|
||||
name: 'viewer.focus',
|
||||
},
|
||||
];
|
||||
|
||||
export default declarations;
|
||||
|
@@ -124,6 +124,7 @@ export interface NoteBodyEditorProps {
|
||||
visiblePanes: string[];
|
||||
keyboardMode: string;
|
||||
tabMovesFocus: boolean;
|
||||
enableTextPatterns: boolean;
|
||||
resourceInfos: ResourceInfos;
|
||||
resourceDirectory: string;
|
||||
locale: string;
|
||||
|
@@ -97,7 +97,15 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
|
||||
|
||||
await initNoteState(n, false);
|
||||
if (event.cancelled) return;
|
||||
setFormNoteRefreshScheduled(0);
|
||||
setFormNoteRefreshScheduled(oldValue => {
|
||||
// If a new refresh was scheduled between initNoteState
|
||||
// and now:
|
||||
if (oldValue !== formNoteRefreshScheduled) {
|
||||
return oldValue;
|
||||
}
|
||||
// A refresh is no longer scheduled
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
await loadNote();
|
||||
|
@@ -1,15 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import getShownPluginEditorView from '@joplin/lib/services/plugins/utils/getShownPluginEditorView';
|
||||
|
||||
// If a plugin editor should be shown for the current note, this function will return the plugin and
|
||||
// associated view.
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
return useMemo(() => {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||
if (editorView) {
|
||||
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||
}
|
||||
return { editorPlugin, editorView };
|
||||
return getShownPluginEditorView(plugins, shownEditorViewIds);
|
||||
}, [plugins, shownEditorViewIds]);
|
||||
};
|
||||
|
@@ -10,6 +10,7 @@ const commandsWithDependencies = [
|
||||
require('../commands/showLocalSearch'),
|
||||
require('../commands/focusElementNoteTitle'),
|
||||
require('../commands/focusElementNoteBody'),
|
||||
require('../commands/focusElementNoteViewer'),
|
||||
require('../commands/focusElementToolbar'),
|
||||
require('../commands/pasteAsText'),
|
||||
];
|
||||
|
@@ -118,6 +118,8 @@ const NoteList = (props: Props) => {
|
||||
props.notes.length,
|
||||
listRenderer.flow,
|
||||
itemsPerLine,
|
||||
props.showCompletedTodos,
|
||||
props.uncompletedTodosOnTop,
|
||||
);
|
||||
|
||||
useItemCss(listRenderer.itemCss);
|
||||
|
@@ -23,6 +23,8 @@ const useOnKeyDown = (
|
||||
noteCount: number,
|
||||
flow: ItemFlow,
|
||||
itemsPerLine: number,
|
||||
showCompletedTodos: boolean,
|
||||
uncompletedTodosOnTop: boolean,
|
||||
) => {
|
||||
const scrollNoteIndex = useCallback((visibleItemCount: number, key: KeyboardEventKey, ctrlKey: boolean, metaKey: boolean, noteIndex: number) => {
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
@@ -142,13 +144,32 @@ const useOnKeyDown = (
|
||||
const todos = selectedNotes.filter(n => !!n.is_todo);
|
||||
if (!todos.length) return;
|
||||
|
||||
const firstNoteIndex = notes.findIndex(n => n.id === todos[0].id);
|
||||
let nextSelectedNoteIndex = firstNoteIndex + 1;
|
||||
if (nextSelectedNoteIndex > notes.length - 1) nextSelectedNoteIndex = notes.length - 1;
|
||||
const nextSelectedNote = nextSelectedNoteIndex >= 0 ? notes[nextSelectedNoteIndex] : todos[0];
|
||||
|
||||
for (let i = 0; i < todos.length; i++) {
|
||||
const toggledTodo = Note.toggleTodoCompleted(todos[i]);
|
||||
await Note.save(toggledTodo);
|
||||
}
|
||||
|
||||
// When the settings `uncompletedTodosOnTop` or `showCompletedTodos` are enabled, the
|
||||
// note that got set as completed or uncompleted is going to disappear from view,
|
||||
// possibly hidden or moved to the top or bottom of the note list. It is assumed that
|
||||
// the user does not want to keep that note selected since the to-do is indeed
|
||||
// "completed". And by keeping that selection, the cursor would jump, making you lose
|
||||
// context if you have multiple to-dos that need to be ticked. For that reason we set
|
||||
// the selection to the next note in the list, which also ensures that the scroll
|
||||
// position doesn't change. This is the same behaviour as when deleting a note.
|
||||
const maintainScrollPosition = !showCompletedTodos || uncompletedTodosOnTop;
|
||||
|
||||
if (maintainScrollPosition) {
|
||||
dispatch({ type: 'NOTE_SELECT', noteId: nextSelectedNote.id });
|
||||
}
|
||||
|
||||
dispatch({ type: 'NOTE_SORT' });
|
||||
focusNote(todos[0].id);
|
||||
if (!maintainScrollPosition) focusNote(todos[0].id);
|
||||
const wasCompleted = !!todos[0].todo_completed;
|
||||
announceForAccessibility(!wasCompleted ? _('Complete') : _('Incomplete'));
|
||||
}
|
||||
@@ -171,7 +192,7 @@ const useOnKeyDown = (
|
||||
type: 'NOTE_SELECT_ALL',
|
||||
});
|
||||
}
|
||||
}, [moveNote, focusNote, visibleItemCount, scrollNoteIndex, makeItemIndexVisible, notes, selectedNoteIds, activeNoteId, dispatch, flow, itemsPerLine]);
|
||||
}, [moveNote, focusNote, visibleItemCount, scrollNoteIndex, makeItemIndexVisible, notes, selectedNoteIds, activeNoteId, dispatch, flow, itemsPerLine, showCompletedTodos, uncompletedTodosOnTop]);
|
||||
|
||||
|
||||
return onKeyDown;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useState, useRef, useMemo } from 'react';
|
||||
|
||||
const useScroll = (itemsPerLine: number, noteCount: number, itemSize: Size, listSize: Size, listRef: React.MutableRefObject<HTMLDivElement>) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
@@ -29,36 +28,36 @@ const useScroll = (itemsPerLine: number, noteCount: number, itemSize: Size, list
|
||||
// but still fails now and then. Setting it after 500ms would probably work
|
||||
// reliably but it's too slow so it makes sense to do it in an interval.
|
||||
|
||||
const setScrollTopLikeYouMeanItTimer = useRef(null);
|
||||
const setScrollTopLikeYouMeanItStartTime = useRef(0);
|
||||
const setScrollTopLikeYouMeanIt = useCallback((newScrollTop: number) => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItStartTime.current = Date.now();
|
||||
// const setScrollTopLikeYouMeanItTimer = useRef(null);
|
||||
// const setScrollTopLikeYouMeanItStartTime = useRef(0);
|
||||
// const setScrollTopLikeYouMeanIt = useCallback((newScrollTop: number) => {
|
||||
// if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
// setScrollTopLikeYouMeanItStartTime.current = Date.now();
|
||||
|
||||
listRef.current.scrollTop = newScrollTop;
|
||||
lastScrollSetTime.current = Date.now();
|
||||
// listRef.current.scrollTop = newScrollTop;
|
||||
// lastScrollSetTime.current = Date.now();
|
||||
|
||||
setScrollTopLikeYouMeanItTimer.current = shim.setInterval(() => {
|
||||
if (!listRef.current) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
return;
|
||||
}
|
||||
// setScrollTopLikeYouMeanItTimer.current = shim.setInterval(() => {
|
||||
// if (!listRef.current) {
|
||||
// shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
// setScrollTopLikeYouMeanItTimer.current = null;
|
||||
// return;
|
||||
// }
|
||||
|
||||
listRef.current.scrollTop = newScrollTop;
|
||||
lastScrollSetTime.current = Date.now();
|
||||
// listRef.current.scrollTop = newScrollTop;
|
||||
// lastScrollSetTime.current = Date.now();
|
||||
|
||||
if (Date.now() - setScrollTopLikeYouMeanItStartTime.current > 1000) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}
|
||||
}, 10);
|
||||
}, [listRef]);
|
||||
// if (Date.now() - setScrollTopLikeYouMeanItStartTime.current > 1000) {
|
||||
// shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
// setScrollTopLikeYouMeanItTimer.current = null;
|
||||
// }
|
||||
// }, 10);
|
||||
// }, [listRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
// setScrollTopLikeYouMeanItTimer.current = null;
|
||||
// }, []);
|
||||
|
||||
const makeItemIndexVisible = useCallback((itemIndex: number) => {
|
||||
const lineTopFloat = scrollTop / itemSize.height;
|
||||
@@ -83,13 +82,17 @@ const useScroll = (itemsPerLine: number, noteCount: number, itemSize: Size, list
|
||||
if (newScrollTop > maxScrollTop) newScrollTop = maxScrollTop;
|
||||
|
||||
setScrollTop(newScrollTop);
|
||||
setScrollTopLikeYouMeanIt(newScrollTop);
|
||||
}, [itemsPerLine, noteCount, itemSize.height, scrollTop, listSize.height, maxScrollTop, setScrollTopLikeYouMeanIt]);
|
||||
listRef.current.scrollTop = newScrollTop;
|
||||
lastScrollSetTime.current = Date.now();
|
||||
// setScrollTopLikeYouMeanIt(newScrollTop);
|
||||
}, [itemsPerLine, noteCount, itemSize.height, scrollTop, listSize.height, maxScrollTop, listRef]); // , setScrollTopLikeYouMeanIt]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onScroll = useCallback((event: any) => {
|
||||
// console.info('ON SCROLL', event.target.scrollTop, 'Ignore:', Date.now() - lastScrollSetTime.current < 500);
|
||||
|
||||
// Ignore the scroll event if it has just been set programmatically.
|
||||
if (Date.now() - lastScrollSetTime.current < 500) return;
|
||||
if (Date.now() - lastScrollSetTime.current < 10) return;
|
||||
setScrollTop(event.target.scrollTop);
|
||||
}, []);
|
||||
|
||||
|
@@ -40,6 +40,12 @@ const useVisibleRange = (itemsPerLine: number, scrollTop: number, listSize: Size
|
||||
return Math.ceil(noteCount / itemsPerLine);
|
||||
}, [noteCount, itemsPerLine]);
|
||||
|
||||
// Note: Leave this here to test the note list scroll behaviour. Also add "item.index" to the
|
||||
// rows in defaultListRenderer to check whether the value here matches what's being displayed.
|
||||
// `useScroll` can also be changed to display the effective scroll value.
|
||||
|
||||
// console.info('=======================================');
|
||||
// console.info('scrollTop', scrollTop);
|
||||
// console.info('itemsPerLine', itemsPerLine);
|
||||
// console.info('listSize.height', listSize.height);
|
||||
// console.info('itemSize.height', itemSize.height);
|
||||
@@ -52,6 +58,7 @@ const useVisibleRange = (itemsPerLine: number, scrollTop: number, listSize: Size
|
||||
// console.info('endLineIndex', endLineIndex);
|
||||
// console.info('totalLineCount', totalLineCount);
|
||||
// console.info('visibleItemCount', visibleItemCount);
|
||||
// console.info('=======================================');
|
||||
|
||||
return [startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount];
|
||||
};
|
||||
|
@@ -37,6 +37,9 @@ interface State {
|
||||
formNote: FormNote;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editedValue: any;
|
||||
isValid: {
|
||||
location: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const uniqueId = (key: string) => `note-properties-dialog-${key}`;
|
||||
@@ -60,6 +63,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
|
||||
this.revisionsLink_click = this.revisionsLink_click.bind(this);
|
||||
this.buttonRow_click = this.buttonRow_click.bind(this);
|
||||
this.locationOnChange = this.locationOnChange.bind(this);
|
||||
this.okButton = React.createRef();
|
||||
this.inputRef = React.createRef();
|
||||
|
||||
@@ -67,6 +71,9 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
formNote: null,
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
isValid: {
|
||||
location: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.keyToLabel_ = {
|
||||
@@ -195,6 +202,17 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
this.styles_.invalidInput = {
|
||||
border: '1px solid',
|
||||
borderColor: theme.colorWarn,
|
||||
};
|
||||
|
||||
this.styles_.invalidMessage = {
|
||||
marginTop: '0.3em',
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 0.9,
|
||||
};
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
@@ -276,6 +294,24 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public async locationOnChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({ editedValue: event.target.value });
|
||||
if (!event.target.value) {
|
||||
this.setState({ isValid: { ...this.state.isValid, location: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.value.includes(',')) {
|
||||
const [lat, log] = event.target.value.split(',');
|
||||
if (parseFloat(lat) < 90 && parseFloat(lat) > -90 && parseFloat(log) < 180 && parseFloat(log) > -180) {
|
||||
this.setState({ isValid: { ...this.state.isValid, location: true } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ isValid: { ...this.state.isValid, location: false } });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public createNoteField(key: keyof FormNote, value: any) {
|
||||
const styles = this.styles(this.props.themeId);
|
||||
@@ -288,8 +324,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
let editCompIcon = null;
|
||||
let editComDescription = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onKeyDown = (event: any) => {
|
||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.keyCode === 13) {
|
||||
void this.saveProperty();
|
||||
} else if (event.keyCode === 27) {
|
||||
@@ -315,6 +350,30 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
};
|
||||
editCompIcon = 'fa-save';
|
||||
editComDescription = _('Save changes');
|
||||
} else if (this.state.editedKey === 'location') {
|
||||
controlComp = (
|
||||
<React.Fragment>
|
||||
<input
|
||||
defaultValue={value}
|
||||
type="text"
|
||||
ref={this.inputRef}
|
||||
onChange={this.locationOnChange}
|
||||
onKeyDown={event => onKeyDown(event)}
|
||||
style={this.state.isValid.location ? styles.input : { ...styles.input, ...styles.invalidInput }}
|
||||
id={uniqueId(key)}
|
||||
name={uniqueId(key)}
|
||||
aria-invalid={!this.state.isValid.location}
|
||||
/>
|
||||
{
|
||||
this.state.isValid.location ? null
|
||||
: <React.Fragment>
|
||||
<div aria-live='polite' style={styles.invalidMessage}>
|
||||
{_('Invalid format. E.g.: 48.8581372, 2.2926735')}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
controlComp = (
|
||||
<input
|
||||
|
@@ -28,6 +28,7 @@ export interface NoteViewerControl {
|
||||
domReady(): boolean;
|
||||
setHtml(html: string, options: SetHtmlOptions): void;
|
||||
send(channel: string, arg0?: unknown, arg1?: unknown): void;
|
||||
focusLine(editorLine: number): void;
|
||||
focus(): void;
|
||||
hasFocus(): boolean;
|
||||
}
|
||||
@@ -107,6 +108,10 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
|
||||
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'focusLine') {
|
||||
win.postMessage({ target: 'webview', name: 'focusLine', data: { line: arg0 } }, '*');
|
||||
}
|
||||
|
||||
// External code should use .setHtml (rather than send('setHtml', ...))
|
||||
if (channel === 'setHtml') {
|
||||
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
|
||||
@@ -139,6 +144,15 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
|
||||
hasFocus: () => {
|
||||
return webviewRef.current?.contains(parentDoc.activeElement);
|
||||
},
|
||||
focusLine: (lineNumber: number) => {
|
||||
if (webviewRef.current) {
|
||||
focus('NoteTextViewer::focusLine', webviewRef.current);
|
||||
// A timeout seems necessary after focusing the viewer to prevent focus from jumping to the top
|
||||
setTimeout(() => {
|
||||
result.send('focusLine', lineNumber);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [parentDoc]);
|
||||
|
19
packages/app-desktop/gui/NotyfContext.js
Normal file
19
packages/app-desktop/gui/NotyfContext.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
// Based on https://github.com/caroso1222/notyf/blob/master/recipes/react.md
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const React = require('react');
|
||||
const notyf_1 = require('notyf');
|
||||
const types_1 = require('@joplin/lib/services/plugins/api/types');
|
||||
exports.default = React.createContext(new notyf_1.Notyf({
|
||||
// Set your global Notyf configuration here
|
||||
duration: 6000,
|
||||
types: [
|
||||
{
|
||||
type: types_1.ToastType.Info,
|
||||
icon: false,
|
||||
className: 'notyf__toast--info',
|
||||
background: 'blue', // Need to set a background, otherwise Notyf won't create the background element. But the color will be overriden in CSS.
|
||||
},
|
||||
],
|
||||
}));
|
||||
// # sourceMappingURL=NotyfContext.js.map
|
@@ -1,20 +0,0 @@
|
||||
// Based on https://github.com/caroso1222/notyf/blob/master/recipes/react.md
|
||||
|
||||
import * as React from 'react';
|
||||
import { Notyf } from 'notyf';
|
||||
import { ToastType } from '@joplin/lib/services/plugins/api/types';
|
||||
|
||||
export default React.createContext(
|
||||
new Notyf({
|
||||
// Set your global Notyf configuration here
|
||||
duration: 6000,
|
||||
types: [
|
||||
{
|
||||
type: ToastType.Info,
|
||||
icon: false,
|
||||
className: 'notyf__toast--info',
|
||||
background: 'blue', // Need to set a background, otherwise Notyf won't create the background element. But the color will be overriden in CSS.
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
@@ -1,8 +1,8 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
import NotyfContext from '../NotyfContext';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import { Toast, ToastType } from '@joplin/lib/services/plugins/api/types';
|
||||
import { INotyfNotificationOptions } from 'notyf';
|
||||
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
|
||||
import { NotificationType } from '../PopupNotification/types';
|
||||
|
||||
const emptyToast = (): Toast => {
|
||||
return {
|
||||
@@ -19,26 +19,23 @@ interface Props {
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const notyfContext = useContext(NotyfContext);
|
||||
const popupManager = useContext(PopupNotificationContext);
|
||||
|
||||
const toast = useMemo(() => {
|
||||
const toast: Toast = props.toast ? props.toast : emptyToast();
|
||||
return toast;
|
||||
}, [props.toast]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
useEffect(() => {
|
||||
if (!toast.message) return;
|
||||
|
||||
const options: Partial<INotyfNotificationOptions> = {
|
||||
type: toast.type,
|
||||
message: toast.message,
|
||||
duration: toast.duration,
|
||||
};
|
||||
|
||||
notyfContext.open(options);
|
||||
popupManager.createPopup(() => toast.message, {
|
||||
type: toast.type as string as NotificationType,
|
||||
}).scheduleDismiss(toast.duration);
|
||||
// toast.timestamp needs to be included in the dependency list to allow
|
||||
// showing multiple toasts with the same message, one after another.
|
||||
// See https://github.com/laurent22/joplin/issues/11783
|
||||
}, [toast.message, toast.duration, toast.type, toast.timestamp, notyfContext]);
|
||||
}, [toast.message, toast.duration, toast.type, toast.timestamp, popupManager]);
|
||||
|
||||
return <div style={{ display: 'none' }}/>;
|
||||
};
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { NotificationType } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
key: string;
|
||||
type: NotificationType;
|
||||
dismissing: boolean;
|
||||
popup: boolean;
|
||||
}
|
||||
|
||||
const NotificationItem: React.FC<Props> = props => {
|
||||
const [iconClassName, iconLabel] = (() => {
|
||||
if (props.type === NotificationType.Success) {
|
||||
return ['fas fa-check', _('Success')];
|
||||
}
|
||||
if (props.type === NotificationType.Error) {
|
||||
return ['fas fa-times', _('Error')];
|
||||
}
|
||||
if (props.type === NotificationType.Info) {
|
||||
return ['fas fa-info', _('Info')];
|
||||
}
|
||||
return ['', ''];
|
||||
})();
|
||||
|
||||
const containerModifier = (() => {
|
||||
if (props.type === NotificationType.Success) return '-success';
|
||||
if (props.type === NotificationType.Error) return '-error';
|
||||
if (props.type === NotificationType.Info) return '-info';
|
||||
return '';
|
||||
})();
|
||||
|
||||
const icon = <i
|
||||
role='img'
|
||||
aria-label={iconLabel}
|
||||
className={`icon ${iconClassName}`}
|
||||
/>;
|
||||
|
||||
return <li
|
||||
role={props.popup ? 'alert' : undefined}
|
||||
className={`popup-notification-item ${containerModifier} ${props.dismissing ? '-dismissing' : ''}`}
|
||||
>
|
||||
{iconClassName ? icon : null}
|
||||
<div className='ripple'/>
|
||||
<div className='content'>
|
||||
{props.children}
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default NotificationItem;
|
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { VisibleNotificationsContext } from './PopupNotificationProvider';
|
||||
import NotificationItem from './NotificationItem';
|
||||
import { useContext } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {}
|
||||
|
||||
// This component displays the popups managed by PopupNotificationContext.
|
||||
// This allows popups to be shown in multiple windows at the same time.
|
||||
const PopupNotificationList: React.FC<Props> = () => {
|
||||
const popupSpecs = useContext(VisibleNotificationsContext);
|
||||
const popups = [];
|
||||
for (const spec of popupSpecs) {
|
||||
if (spec.dismissed) continue;
|
||||
|
||||
popups.push(
|
||||
<NotificationItem
|
||||
key={spec.key}
|
||||
type={spec.type}
|
||||
dismissing={!!spec.dismissAt}
|
||||
popup={true}
|
||||
>{spec.content()}</NotificationItem>,
|
||||
);
|
||||
}
|
||||
popups.reverse();
|
||||
|
||||
if (popups.length) {
|
||||
return <ul
|
||||
className='popup-notification-list -overlay'
|
||||
role='group'
|
||||
aria-label={_('Notifications')}
|
||||
>
|
||||
{popups}
|
||||
</ul>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default PopupNotificationList;
|
@@ -0,0 +1,122 @@
|
||||
import * as React from 'react';
|
||||
import { createContext, useMemo, useRef, useState } from 'react';
|
||||
import { NotificationType, PopupHandle, PopupControl as PopupManager } from './types';
|
||||
import { Hour, msleep } from '@joplin/utils/time';
|
||||
|
||||
export const PopupNotificationContext = createContext<PopupManager|null>(null);
|
||||
export const VisibleNotificationsContext = createContext<PopupSpec[]>([]);
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface PopupSpec {
|
||||
key: string;
|
||||
dismissAt?: number;
|
||||
dismissed: boolean;
|
||||
type: NotificationType;
|
||||
content: ()=> React.ReactNode;
|
||||
}
|
||||
|
||||
const PopupNotificationProvider: React.FC<Props> = props => {
|
||||
const [popupSpecs, setPopupSpecs] = useState<PopupSpec[]>([]);
|
||||
const nextPopupKey = useRef(0);
|
||||
|
||||
const popupManager = useMemo((): PopupManager => {
|
||||
const removeOldPopups = () => {
|
||||
// The WCAG allows dismissing notifications older than 20 hours.
|
||||
setPopupSpecs(popups => popups.filter(popup => {
|
||||
if (!popup.dismissed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dismissedRecently = popup.dismissAt > performance.now() - Hour * 20;
|
||||
return dismissedRecently;
|
||||
}));
|
||||
};
|
||||
|
||||
const removePopupWithKey = (key: string) => {
|
||||
setPopupSpecs(popups => popups.filter(p => p.key !== key));
|
||||
};
|
||||
|
||||
type UpdatePopupCallback = (popup: PopupSpec)=> PopupSpec;
|
||||
const updatePopupWithKey = (key: string, updateCallback: UpdatePopupCallback) => {
|
||||
setPopupSpecs(popups => popups.map(p => {
|
||||
if (p.key === key) {
|
||||
return updateCallback(p);
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const dismissAnimationDelay = 600;
|
||||
const dismissPopup = async (key: string) => {
|
||||
// Start the dismiss animation
|
||||
updatePopupWithKey(key, popup => ({
|
||||
...popup,
|
||||
dismissAt: performance.now() + dismissAnimationDelay,
|
||||
}));
|
||||
|
||||
await msleep(dismissAnimationDelay);
|
||||
|
||||
updatePopupWithKey(key, popup => ({
|
||||
...popup,
|
||||
dismissed: true,
|
||||
}));
|
||||
removeOldPopups();
|
||||
};
|
||||
|
||||
const dismissAndRemovePopup = async (key: string) => {
|
||||
await dismissPopup(key);
|
||||
removePopupWithKey(key);
|
||||
};
|
||||
|
||||
const manager: PopupManager = {
|
||||
createPopup: (content, { type } = {}): PopupHandle => {
|
||||
const key = `popup-${nextPopupKey.current++}`;
|
||||
const newPopup: PopupSpec = {
|
||||
key,
|
||||
content,
|
||||
type,
|
||||
dismissed: false,
|
||||
};
|
||||
|
||||
setPopupSpecs(popups => {
|
||||
const newPopups = [...popups];
|
||||
|
||||
// Replace the existing popup, if it exists
|
||||
const insertIndex = newPopups.findIndex(p => p.key === key);
|
||||
if (insertIndex === -1) {
|
||||
newPopups.push(newPopup);
|
||||
} else {
|
||||
newPopups.splice(insertIndex, 1, newPopup);
|
||||
}
|
||||
|
||||
return newPopups;
|
||||
});
|
||||
|
||||
const handle: PopupHandle = {
|
||||
remove() {
|
||||
void dismissAndRemovePopup(key);
|
||||
},
|
||||
scheduleDismiss(delay = 5_500) {
|
||||
setTimeout(() => {
|
||||
void dismissPopup(key);
|
||||
}, delay);
|
||||
},
|
||||
};
|
||||
return handle;
|
||||
},
|
||||
};
|
||||
return manager;
|
||||
}, []);
|
||||
|
||||
return <PopupNotificationContext.Provider value={popupManager}>
|
||||
<VisibleNotificationsContext.Provider value={popupSpecs}>
|
||||
{props.children}
|
||||
</VisibleNotificationsContext.Provider>
|
||||
</PopupNotificationContext.Provider>;
|
||||
};
|
||||
|
||||
export default PopupNotificationProvider;
|
22
packages/app-desktop/gui/PopupNotification/types.ts
Normal file
22
packages/app-desktop/gui/PopupNotification/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type PopupHandle = {
|
||||
remove(): void;
|
||||
scheduleDismiss(delay?: number): void;
|
||||
};
|
||||
|
||||
export enum NotificationType {
|
||||
Info = 'info',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type NotificationContentCallback = ()=> React.ReactNode;
|
||||
|
||||
export interface PopupOptions {
|
||||
type?: NotificationType;
|
||||
}
|
||||
|
||||
export interface PopupControl {
|
||||
createPopup(content: NotificationContentCallback, props?: PopupOptions): PopupHandle;
|
||||
}
|
@@ -30,6 +30,7 @@ import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsA
|
||||
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
|
||||
import bridge from '../services/bridge';
|
||||
import EditorWindow from './NoteEditor/EditorWindow';
|
||||
import PopupNotificationProvider from './PopupNotification/PopupNotificationProvider';
|
||||
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
||||
|
||||
interface Props {
|
||||
@@ -197,13 +198,15 @@ class RootComponent extends React.Component<Props, any> {
|
||||
return (
|
||||
<StyleSheetManager disableVendorPrefixes>
|
||||
<ThemeProvider theme={theme}>
|
||||
<StyleSheetContainer/>
|
||||
<MenuBar/>
|
||||
<GlobalStyle/>
|
||||
<WindowCommandsAndDialogs windowId={defaultWindowId} />
|
||||
<Navigator style={navigatorStyle} screens={screens} className={`profile-${this.props.profileConfigCurrentProfileId}`} />
|
||||
{this.renderSecondaryWindows()}
|
||||
{this.renderModalMessage(this.modalDialogProps())}
|
||||
<PopupNotificationProvider>
|
||||
<StyleSheetContainer/>
|
||||
<MenuBar/>
|
||||
<GlobalStyle/>
|
||||
<WindowCommandsAndDialogs windowId={defaultWindowId} />
|
||||
<Navigator style={navigatorStyle} screens={screens} className={`profile-${this.props.profileConfigCurrentProfileId}`} />
|
||||
{this.renderSecondaryWindows()}
|
||||
{this.renderModalMessage(this.modalDialogProps())}
|
||||
</PopupNotificationProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import areAllFoldersCollapsed from '@joplin/lib/models/utils/areAllFoldersCollapsed';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
@@ -41,6 +42,10 @@ const FolderAndTagList: React.FC<Props> = props => {
|
||||
listItems: listItems,
|
||||
});
|
||||
|
||||
const allFoldersCollapsed = useMemo(() => {
|
||||
return areAllFoldersCollapsed(props.folders, props.collapsedFolderIds);
|
||||
}, [props.collapsedFolderIds, props.folders]);
|
||||
|
||||
const listContainerRef = useRef<HTMLDivElement|null>(null);
|
||||
const onRenderItem = useOnRenderItem({
|
||||
...props,
|
||||
@@ -67,7 +72,7 @@ const FolderAndTagList: React.FC<Props> = props => {
|
||||
const listHeight = useElementHeight(itemListContainer);
|
||||
const listStyle = useMemo(() => ({ height: listHeight }), [listHeight]);
|
||||
|
||||
const onRenderContentWrapper = useOnRenderListWrapper({ selectedIndex, onKeyDown: onKeyEventHandler });
|
||||
const onRenderContentWrapper = useOnRenderListWrapper({ allFoldersCollapsed, selectedIndex, onKeyDown: onKeyEventHandler });
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@@ -417,6 +417,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
key={item.key}
|
||||
anchorRef={anchorRef}
|
||||
selected={selected}
|
||||
item={item}
|
||||
index={index}
|
||||
itemCount={itemCount}
|
||||
/>;
|
||||
@@ -425,7 +426,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
<ListItemWrapper
|
||||
key={item.key}
|
||||
containerRef={anchorRef}
|
||||
depth={0}
|
||||
depth={1}
|
||||
selected={selected}
|
||||
itemIndex={index}
|
||||
itemCount={itemCount}
|
||||
|
@@ -6,16 +6,40 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
interface Props {
|
||||
selectedIndex: number;
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
allFoldersCollapsed: boolean;
|
||||
}
|
||||
|
||||
const onAddFolderButtonClick = () => {
|
||||
void CommandService.instance().execute('newFolder');
|
||||
};
|
||||
|
||||
const onToggleAllFolders = (allFoldersCollapsed: boolean) => {
|
||||
void CommandService.instance().execute('toggleAllFolders', !allFoldersCollapsed);
|
||||
};
|
||||
|
||||
interface CollapseExpandAllButtonProps {
|
||||
allFoldersCollapsed: boolean;
|
||||
}
|
||||
|
||||
const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
|
||||
// To allow it to be accessed by accessibility tools, the toggle button
|
||||
// is not included in the portion of the list with role='tree'.
|
||||
const icon = props.allFoldersCollapsed ? 'far fa-caret-square-right' : 'far fa-caret-square-down';
|
||||
const label = props.allFoldersCollapsed ? _('Expand all notebooks') : _('Collapse all notebooks');
|
||||
|
||||
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall'>
|
||||
<i
|
||||
aria-label={label}
|
||||
role='img'
|
||||
className={icon}
|
||||
/>
|
||||
</button>;
|
||||
};
|
||||
|
||||
const NewFolderButton = () => {
|
||||
// To allow it to be accessed by accessibility tools, the new folder button
|
||||
// is not included in the portion of the list with role='tree'.
|
||||
return <button onClick={onAddFolderButtonClick} className='new-folder-button'>
|
||||
return <button onClick={onAddFolderButtonClick} className='sidebar-header-button -newfolder'>
|
||||
<i
|
||||
aria-label={_('New notebook')}
|
||||
role='img'
|
||||
@@ -24,22 +48,23 @@ const NewFolderButton = () => {
|
||||
</button>;
|
||||
};
|
||||
|
||||
const useOnRenderListWrapper = ({ selectedIndex, onKeyDown }: Props) => {
|
||||
const useOnRenderListWrapper = (props: Props) => {
|
||||
return useCallback((listItems: React.ReactNode[]) => {
|
||||
const listHasValidSelection = selectedIndex >= 0;
|
||||
const listHasValidSelection = props.selectedIndex >= 0;
|
||||
const allowContainerFocus = !listHasValidSelection;
|
||||
return <>
|
||||
<CollapseExpandAllButton allFoldersCollapsed={props.allFoldersCollapsed}/>
|
||||
<NewFolderButton/>
|
||||
<div
|
||||
role='tree'
|
||||
className='sidebar-list-items-wrapper'
|
||||
tabIndex={allowContainerFocus ? 0 : undefined}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDown={props.onKeyDown}
|
||||
>
|
||||
{...listItems}
|
||||
</div>
|
||||
</>;
|
||||
}, [selectedIndex, onKeyDown]);
|
||||
}, [props.selectedIndex, props.onKeyDown, props.allFoldersCollapsed]);
|
||||
};
|
||||
|
||||
export default useOnRenderListWrapper;
|
||||
|
@@ -12,7 +12,6 @@ interface Props {
|
||||
updateSelectedIndex: SetSelectedIndexCallback;
|
||||
}
|
||||
|
||||
|
||||
const isToggleShortcut = (keyCode: string, selectedItem: ListItem, collapsedFolderIds: string[]) => {
|
||||
if (selectedItem.kind !== ListItemType.Header && selectedItem.kind !== ListItemType.Folder) {
|
||||
return false;
|
||||
@@ -22,6 +21,10 @@ const isToggleShortcut = (keyCode: string, selectedItem: ListItem, collapsedFold
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!selectedItem.hasChildren) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyCode === 'Space') {
|
||||
return true;
|
||||
}
|
||||
@@ -30,6 +33,22 @@ const isToggleShortcut = (keyCode: string, selectedItem: ListItem, collapsedFold
|
||||
return (keyCode === 'ArrowRight') === isCollapsed;
|
||||
};
|
||||
|
||||
const getParentOffset = (childIndex: number, listItems: ListItem[]): number|null => {
|
||||
const childItem = listItems[childIndex];
|
||||
const targetDepth = childItem.depth - 1;
|
||||
|
||||
let indexChange = 0;
|
||||
for (let i = childIndex; i >= 0; i--) {
|
||||
const otherItem = listItems[i];
|
||||
if (otherItem.depth === targetDepth) {
|
||||
return indexChange;
|
||||
}
|
||||
indexChange --;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
const { updateSelectedIndex, listItems, selectedIndex, collapsedFolderIds, dispatch } = props;
|
||||
|
||||
@@ -48,12 +67,21 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
} else if (selectedItem.kind === ListItemType.Header) {
|
||||
toggleHeader(selectedItem.id);
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
|
||||
event.preventDefault();
|
||||
} else if (selectedItem && event.code === 'ArrowLeft') { // Jump to parent
|
||||
const isFolderWithParent = selectedItem.kind === ListItemType.Folder && selectedItem.folder.parent_id;
|
||||
// For now, only allow this shortcut for folders with parents -- jumping to the tags or
|
||||
// folders headers could be confusing.
|
||||
if (isFolderWithParent) {
|
||||
indexChange = getParentOffset(selectedIndex, listItems) ?? 0;
|
||||
}
|
||||
} else if (selectedItem?.hasChildren && event.code === 'ArrowRight') { // Jump to first child
|
||||
indexChange = 1;
|
||||
} else if (event.code === 'ArrowUp') {
|
||||
indexChange = -1;
|
||||
} else if (event.code === 'ArrowDown') {
|
||||
indexChange = 1;
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
|
||||
event.preventDefault();
|
||||
} else if (event.code === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void CommandService.instance().execute('focusElement', 'noteList');
|
||||
|
@@ -20,6 +20,8 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
kind: ListItemType.Tag,
|
||||
tag,
|
||||
key: tag.id,
|
||||
depth: 1,
|
||||
hasChildren: false,
|
||||
};
|
||||
});
|
||||
}, [props.tags]);
|
||||
@@ -38,7 +40,9 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
kind: ListItemType.Folder,
|
||||
folder,
|
||||
hasChildren,
|
||||
depth,
|
||||
// The toplevel headers have depth 1, so the toplevel notebook needs
|
||||
// depth 2.
|
||||
depth: depth + 1,
|
||||
key: folder.id,
|
||||
};
|
||||
});
|
||||
@@ -57,11 +61,13 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
['data-folder-id']: '',
|
||||
},
|
||||
supportsFolderDrop: true,
|
||||
depth: 1,
|
||||
hasChildren: folderItems.items.length > 0,
|
||||
};
|
||||
const foldersSectionContent: ListItem[] = props.folderHeaderIsExpanded ? [
|
||||
{ kind: ListItemType.AllNotes, key: 'all-notes' },
|
||||
{ kind: ListItemType.AllNotes, key: 'all-notes', depth: 2, hasChildren: false },
|
||||
...folderItems.items,
|
||||
{ kind: ListItemType.Spacer, key: 'after-folders-spacer' },
|
||||
{ kind: ListItemType.Spacer, key: 'after-folders-spacer', depth: 1, hasChildren: false },
|
||||
] : [];
|
||||
|
||||
const tagsHeader: HeaderListItem = {
|
||||
@@ -74,6 +80,8 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
onClick: toggleHeader,
|
||||
extraProps: { },
|
||||
supportsFolderDrop: false,
|
||||
depth: 1,
|
||||
hasChildren: tagItems.items.length > 0,
|
||||
};
|
||||
const tagsSectionContent: ListItem[] = props.tagHeaderIsExpanded ? tagItems.items : [];
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import { connect } from 'react-redux';
|
||||
import EmptyExpandLink from './EmptyExpandLink';
|
||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
||||
import { ListItem } from '../types';
|
||||
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
@@ -20,6 +21,7 @@ interface Props {
|
||||
dispatch: Dispatch;
|
||||
anchorRef: ListItemRef;
|
||||
selected: boolean;
|
||||
item: ListItem;
|
||||
index: number;
|
||||
itemCount: number;
|
||||
}
|
||||
@@ -53,7 +55,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
||||
containerRef={props.anchorRef}
|
||||
key="allNotesHeader"
|
||||
selected={props.selected}
|
||||
depth={1}
|
||||
depth={props.item.depth}
|
||||
className={'list-item-container list-item-depth-0 all-notes'}
|
||||
highlightOnHover={true}
|
||||
itemIndex={props.index}
|
||||
|
@@ -6,7 +6,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const EmptyExpandLink: React.FC<Props> = props => {
|
||||
return <a className={`sidebar-expand-link ${props.className ?? ''}`}><ExpandIcon isVisible={false} isExpanded={false}/></a>;
|
||||
return <a className={`sidebar-expand-link ${props.className ?? ''}`}>
|
||||
<ExpandIcon isVisible={false} isExpanded={false}/>
|
||||
</a>;
|
||||
};
|
||||
|
||||
export default EmptyExpandLink;
|
||||
|
@@ -1,15 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
type ExpandIconProps = {
|
||||
interface ExpandIconProps {
|
||||
isExpanded: boolean;
|
||||
isVisible: true;
|
||||
targetTitle: string;
|
||||
}|{
|
||||
isExpanded: boolean;
|
||||
isVisible: false;
|
||||
targetTitle?: string;
|
||||
};
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const ExpandIcon: React.FC<ExpandIconProps> = props => {
|
||||
const classNames = ['sidebar-expand-icon'];
|
||||
@@ -23,12 +18,17 @@ const ExpandIcon: React.FC<ExpandIconProps> = props => {
|
||||
return undefined;
|
||||
}
|
||||
if (props.isExpanded) {
|
||||
return _('Expanded, press space to collapse.');
|
||||
return _('Expanded');
|
||||
}
|
||||
return _('Collapsed, press space to expand.');
|
||||
return _('Collapsed');
|
||||
};
|
||||
const label = getLabel();
|
||||
return <i className={classNames.join(' ')} aria-label={label} role='img'></i>;
|
||||
return <i
|
||||
className={classNames.join(' ')}
|
||||
aria-hidden={!props.isVisible}
|
||||
aria-label={label}
|
||||
role='img'
|
||||
></i>;
|
||||
};
|
||||
|
||||
export default ExpandIcon;
|
||||
|
@@ -5,7 +5,6 @@ import EmptyExpandLink from './EmptyExpandLink';
|
||||
|
||||
interface ExpandLinkProps {
|
||||
folderId: string;
|
||||
folderTitle: string;
|
||||
hasChildren: boolean;
|
||||
isExpanded: boolean;
|
||||
className: string;
|
||||
@@ -15,7 +14,7 @@ interface ExpandLinkProps {
|
||||
const ExpandLink: React.FC<ExpandLinkProps> = props => {
|
||||
return props.hasChildren ? (
|
||||
<a className={`sidebar-expand-link ${props.className}`} data-folder-id={props.folderId} onClick={props.onClick} role='button'>
|
||||
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} targetTitle={props.folderTitle}/>
|
||||
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} />
|
||||
</a>
|
||||
) : (
|
||||
<EmptyExpandLink className={props.className}/>
|
||||
|
@@ -11,6 +11,7 @@ import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import NoteCount from './NoteCount';
|
||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
||||
import { useId } from 'react';
|
||||
|
||||
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
||||
if (!folderIcon) {
|
||||
@@ -65,6 +66,7 @@ function FolderItem(props: FolderItemProps) {
|
||||
if (!showFolderIcon) return null;
|
||||
return renderFolderIcon(folderIcon);
|
||||
};
|
||||
const titleId = useId();
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
@@ -85,9 +87,13 @@ function FolderItem(props: FolderItemProps) {
|
||||
data-folder-id={folderId}
|
||||
data-id={folderId}
|
||||
data-type={ModelType.Folder}
|
||||
// Accessibility labels: Don't include the expand/collapse link in the description,
|
||||
// since this information is already conveyed by aria-* props.
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<StyledListItemAnchor
|
||||
className="list-item"
|
||||
id={titleId}
|
||||
isConflictFolder={folderId === Folder.conflictFolderId()}
|
||||
selected={selected}
|
||||
shareId={shareId}
|
||||
@@ -106,7 +112,6 @@ function FolderItem(props: FolderItemProps) {
|
||||
// title first.
|
||||
className='toggle'
|
||||
hasChildren={hasChildren}
|
||||
folderTitle={folderTitle}
|
||||
folderId={folderId}
|
||||
onClick={onFolderToggleClick_}
|
||||
isExpanded={isExpanded}
|
||||
|
@@ -52,7 +52,7 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
itemCount={props.itemCount}
|
||||
expanded={props.item.expanded}
|
||||
onContextMenu={onContextMenu}
|
||||
depth={0}
|
||||
depth={item.depth}
|
||||
highlightOnHover={false}
|
||||
className='sidebar-header-container'
|
||||
{...item.extraProps}
|
||||
|
@@ -23,7 +23,9 @@ interface Props {
|
||||
draggable?: boolean;
|
||||
'data-folder-id'?: string;
|
||||
'data-id'?: string;
|
||||
'data-tag-id'?: string;
|
||||
'data-type'?: ModelType;
|
||||
'aria-labelledby'?: string;
|
||||
}
|
||||
|
||||
const ListItemWrapper: React.FC<Props> = props => {
|
||||
@@ -40,8 +42,7 @@ const ListItemWrapper: React.FC<Props> = props => {
|
||||
aria-setsize={props.itemCount}
|
||||
aria-selected={props.selected}
|
||||
aria-expanded={props.expanded}
|
||||
// aria-level is 1-based, where depth is zero-based
|
||||
aria-level={props.depth + 1}
|
||||
aria-level={props.depth}
|
||||
tabIndex={props.selected ? 0 : -1}
|
||||
|
||||
onContextMenu={props.onContextMenu}
|
||||
@@ -56,7 +57,9 @@ const ListItemWrapper: React.FC<Props> = props => {
|
||||
style={style}
|
||||
data-folder-id={props['data-folder-id']}
|
||||
data-id={props['data-id']}
|
||||
data-tag-id={props['data-tag-id']}
|
||||
data-type={props['data-type']}
|
||||
aria-labelledby={props['aria-labelledby']}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
@@ -5,4 +5,4 @@
|
||||
@use 'styles/sidebar-expand-link.scss';
|
||||
@use 'styles/sidebar-header-container.scss';
|
||||
@use 'styles/sidebar-spacer-item.scss';
|
||||
@use 'styles/new-folder-button.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
@@ -5,7 +5,10 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: calc(var(--joplin-main-padding) + (var(--depth) * 16px) - 16px);
|
||||
// The top-level folder has depth 2, so we need to subtract for the item
|
||||
// to have the correct padding
|
||||
--absolute-depth: calc(var(--depth) - 2);
|
||||
padding-left: calc(var(--joplin-main-padding) + (var(--absolute-depth) * 16px));
|
||||
background: none;
|
||||
transition: 0.1s;
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
|
||||
.new-folder-button {
|
||||
.sidebar-header-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
|
||||
padding-inline-end: 15px;
|
||||
padding-top: 4px;
|
||||
padding-top: 8px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
|
||||
@@ -22,4 +22,8 @@
|
||||
color: var(--joplin-color-active2);
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.-collapseall {
|
||||
right: 25px;
|
||||
}
|
||||
}
|
@@ -16,9 +16,15 @@ export enum ListItemType {
|
||||
|
||||
interface BaseListItem {
|
||||
key: string;
|
||||
depth: number;
|
||||
hasChildren: boolean;
|
||||
}
|
||||
|
||||
export interface HeaderListItem extends BaseListItem {
|
||||
interface ToplevelListItem extends BaseListItem {
|
||||
depth: 1;
|
||||
}
|
||||
|
||||
export interface HeaderListItem extends ToplevelListItem {
|
||||
kind: ListItemType.Header;
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
@@ -42,10 +48,9 @@ export interface FolderListItem extends BaseListItem {
|
||||
kind: ListItemType.Folder;
|
||||
folder: FolderEntity;
|
||||
hasChildren: boolean;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export interface SpacerListItem extends BaseListItem {
|
||||
export interface SpacerListItem extends ToplevelListItem {
|
||||
kind: ListItemType.Spacer;
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { useContext, useCallback, useMemo, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { StateLastDeletion } from '@joplin/lib/reducer';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import NotyfContext from '../NotyfContext';
|
||||
import { waitForElement } from '@joplin/lib/dom';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Dispatch } from 'redux';
|
||||
import { NotyfNotification } from 'notyf';
|
||||
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
|
||||
import { NotificationType } from '../PopupNotification/types';
|
||||
import TrashNotificationMessage from './TrashNotificationMessage';
|
||||
|
||||
interface Props {
|
||||
lastDeletion: StateLastDeletion;
|
||||
@@ -18,50 +16,29 @@ interface Props {
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
const onCancelClick = async (lastDeletion: StateLastDeletion) => {
|
||||
if (lastDeletion.folderIds.length) {
|
||||
await restoreItems(ModelType.Folder, lastDeletion.folderIds);
|
||||
}
|
||||
|
||||
if (lastDeletion.noteIds.length) {
|
||||
await restoreItems(ModelType.Note, lastDeletion.noteIds);
|
||||
}
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const notyfContext = useContext(NotyfContext);
|
||||
const notificationRef = useRef<NotyfNotification | null>(null);
|
||||
const popupManager = useContext(PopupNotificationContext);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
}, [props.themeId]);
|
||||
const lastDeletionNotificationTimeRef = useRef<number>();
|
||||
lastDeletionNotificationTimeRef.current = props.lastDeletionNotificationTime;
|
||||
|
||||
const notyf = useMemo(() => {
|
||||
const output = notyfContext;
|
||||
output.options.types = notyfContext.options.types.map(type => {
|
||||
if (type.type === 'success') {
|
||||
type.background = theme.backgroundColor5;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(type.icon as any).color = theme.backgroundColor5;
|
||||
}
|
||||
return type;
|
||||
});
|
||||
return output;
|
||||
}, [notyfContext, theme]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onCancelClick = useCallback(async (event: any) => {
|
||||
notyf.dismiss(notificationRef.current);
|
||||
notificationRef.current = null;
|
||||
|
||||
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
|
||||
|
||||
if (lastDeletion.folderIds.length) {
|
||||
await restoreItems(ModelType.Folder, lastDeletion.folderIds);
|
||||
}
|
||||
|
||||
if (lastDeletion.noteIds.length) {
|
||||
await restoreItems(ModelType.Note, lastDeletion.noteIds);
|
||||
}
|
||||
}, [notyf]);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (!props.lastDeletion || props.lastDeletion.timestamp <= props.lastDeletionNotificationTime) return;
|
||||
useEffect(() => {
|
||||
const lastDeletionNotificationTime = lastDeletionNotificationTimeRef.current;
|
||||
if (!props.lastDeletion || props.lastDeletion.timestamp <= lastDeletionNotificationTime) return;
|
||||
|
||||
props.dispatch({ type: 'DELETION_NOTIFICATION_DONE' });
|
||||
|
||||
let msg = '';
|
||||
|
||||
if (props.lastDeletion.folderIds.length) {
|
||||
msg = _('The notebook and its content was successfully moved to the trash.');
|
||||
} else if (props.lastDeletion.noteIds.length) {
|
||||
@@ -70,16 +47,15 @@ export default (props: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
|
||||
const cancelLabel = _('Cancel');
|
||||
|
||||
const notification = notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
|
||||
notificationRef.current = notification;
|
||||
|
||||
const element: HTMLAnchorElement = await waitForElement(document, linkId);
|
||||
if (event.cancelled) return;
|
||||
element.addEventListener('click', onCancelClick);
|
||||
}, [props.lastDeletion, notyf, props.dispatch]);
|
||||
const handleCancelClick = () => {
|
||||
notification.remove();
|
||||
void onCancelClick(props.lastDeletion);
|
||||
};
|
||||
const notification = popupManager.createPopup(() => (
|
||||
<TrashNotificationMessage message={msg} onCancel={handleCancelClick}/>
|
||||
), { type: NotificationType.Success });
|
||||
notification.scheduleDismiss();
|
||||
}, [props.lastDeletion, props.dispatch, popupManager]);
|
||||
|
||||
return <div style={{ display: 'none' }}/>;
|
||||
};
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
message: string;
|
||||
onCancel: ()=> void;
|
||||
}
|
||||
|
||||
const TrashNotificationMessage: React.FC<Props> = props => {
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const onCancel = useCallback(() => {
|
||||
setCancelling(true);
|
||||
props.onCancel();
|
||||
}, [props.onCancel]);
|
||||
|
||||
return <>
|
||||
{props.message}
|
||||
{' '}
|
||||
<button
|
||||
className="link-button"
|
||||
onClick={onCancel}
|
||||
>{cancelling ? _('Cancelling...') : _('Cancel')}</button>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default TrashNotificationMessage;
|
@@ -1,27 +0,0 @@
|
||||
body .notyf {
|
||||
color: var(--joplin-color5);
|
||||
}
|
||||
|
||||
.notyf__toast {
|
||||
|
||||
> .notyf__wrapper {
|
||||
|
||||
> .notyf__message {
|
||||
|
||||
> .cancel {
|
||||
color: var(--joplin-color5);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
> .notyf__icon {
|
||||
|
||||
> .notyf__icon--success {
|
||||
background-color: var(--joplin-color5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,17 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import NotyfContext from '../NotyfContext';
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import { UpdateInfo } from 'electron-updater';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
|
||||
import { NotyfEvent, NotyfNotification } from 'notyf';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { NotificationType } from '../PopupNotification/types';
|
||||
|
||||
interface UpdateNotificationProps {
|
||||
themeId: number;
|
||||
interface Props {
|
||||
}
|
||||
|
||||
export enum UpdateNotificationEvents {
|
||||
@@ -22,111 +20,61 @@ export enum UpdateNotificationEvents {
|
||||
|
||||
const changelogLink = 'https://github.com/laurent22/joplin/releases';
|
||||
|
||||
window.openChangelogLink = () => {
|
||||
const openChangelogLink = () => {
|
||||
shim.openUrl(changelogLink);
|
||||
};
|
||||
|
||||
const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
|
||||
const notyfContext = useContext(NotyfContext);
|
||||
const notificationRef = useRef<NotyfNotification | null>(null); // Use ref to hold the current notification
|
||||
|
||||
const theme = useMemo(() => themeStyle(themeId), [themeId]);
|
||||
|
||||
const notyf = useMemo(() => {
|
||||
const output = notyfContext;
|
||||
output.options.types = notyfContext.options.types.map(type => {
|
||||
if (type.type === 'success') {
|
||||
type.background = theme.backgroundColor5;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(type.icon as any).color = theme.backgroundColor5;
|
||||
}
|
||||
return type;
|
||||
});
|
||||
return output;
|
||||
}, [notyfContext, theme]);
|
||||
|
||||
const handleDismissNotification = useCallback(() => {
|
||||
notyf.dismiss(notificationRef.current);
|
||||
notificationRef.current = null;
|
||||
}, [notyf]);
|
||||
|
||||
const handleApplyUpdate = useCallback(() => {
|
||||
ipcRenderer.send('apply-update-now');
|
||||
handleDismissNotification();
|
||||
}, [handleDismissNotification]);
|
||||
const handleApplyUpdate = () => {
|
||||
ipcRenderer.send('apply-update-now');
|
||||
};
|
||||
|
||||
const UpdateNotification: React.FC<Props> = () => {
|
||||
const popupManager = useContext(PopupNotificationContext);
|
||||
|
||||
const handleUpdateDownloaded = useCallback((_event: IpcRendererEvent, info: UpdateInfo) => {
|
||||
if (notificationRef.current) return;
|
||||
|
||||
const updateAvailableHtml = htmlentities(_('A new update (%s) is available', info.version));
|
||||
const seeChangelogHtml = htmlentities(_('See changelog'));
|
||||
const restartNowHtml = htmlentities(_('Restart now'));
|
||||
const updateLaterHtml = htmlentities(_('Update later'));
|
||||
|
||||
const messageHtml = `
|
||||
<div class="update-notification" style="color: ${theme.color2};">
|
||||
${updateAvailableHtml} <a href="#" onclick="openChangelogLink()" style="color: ${theme.color2};">${seeChangelogHtml}</a>
|
||||
<div style="display: flex; gap: 10px; margin-top: 8px;">
|
||||
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.ApplyUpdate}'))" class="notyf__button notyf__button--confirm" style="color: ${theme.color2};">${restartNowHtml}</button>
|
||||
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.Dismiss}'))" class="notyf__button notyf__button--dismiss" style="color: ${theme.color2};">${updateLaterHtml}</button>
|
||||
const notification = popupManager.createPopup(() => (
|
||||
<div className='update-notification'>
|
||||
{_('A new update (%s) is available', info.version)}
|
||||
<button className='link-button' onClick={openChangelogLink}>{
|
||||
_('See changelog')
|
||||
}</button>
|
||||
<div className='buttons'>
|
||||
<Button
|
||||
level={ButtonLevel.Tertiary}
|
||||
onClick={() => {
|
||||
notification.remove();
|
||||
handleApplyUpdate();
|
||||
}}
|
||||
title={_('Restart now')}
|
||||
/>
|
||||
<Button
|
||||
level={ButtonLevel.Tertiary}
|
||||
onClick={() => notification.remove()}
|
||||
title={_('Update later')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const notification: NotyfNotification = notyf.open({
|
||||
type: 'success',
|
||||
message: messageHtml,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
));
|
||||
}, [popupManager]);
|
||||
|
||||
const handleUpdateNotAvailable = useCallback(() => {
|
||||
if (notificationRef.current) return;
|
||||
|
||||
const noUpdateMessageHtml = htmlentities(_('No updates available'));
|
||||
|
||||
const messageHtml = `
|
||||
<div class="update-notification" style="color: ${theme.color2};">
|
||||
${noUpdateMessageHtml}
|
||||
const notification = popupManager.createPopup(() => (
|
||||
<div className='update-notification'>
|
||||
{_('No updates available')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const notification: NotyfNotification = notyf.open({
|
||||
type: 'success',
|
||||
message: messageHtml,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
notification.on(NotyfEvent.Dismiss, () => {
|
||||
notificationRef.current = null;
|
||||
});
|
||||
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
), { type: NotificationType.Info });
|
||||
notification.scheduleDismiss();
|
||||
}, [popupManager]);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
};
|
||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]);
|
||||
}, [handleUpdateDownloaded, handleUpdateNotAvailable]);
|
||||
|
||||
|
||||
return (
|
||||
|
@@ -1,27 +1,11 @@
|
||||
.update-notification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.notyf__button {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
> .buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ import useWindowCommands from './utils/useWindowCommands';
|
||||
import PluginDialogs from './PluginDialogs';
|
||||
import useSyncDialogState from './utils/useSyncDialogState';
|
||||
import AppDialogs from './AppDialogs';
|
||||
import PopupNotificationList from '../PopupNotification/PopupNotificationList';
|
||||
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
|
||||
@@ -113,7 +114,9 @@ const WindowCommandsAndDialogs: React.FC<Props> = props => {
|
||||
const dialogInfo = PluginManager.instance().pluginDialogToShow(props.pluginsLegacy);
|
||||
const pluginDialog = !dialogInfo ? null : <dialogInfo.Dialog {...dialogInfo.props} />;
|
||||
|
||||
const { noteContentPropertiesDialogOptions, notePropertiesDialogOptions, shareNoteDialogOptions, shareFolderDialogOptions, promptOptions } = dialogState;
|
||||
const {
|
||||
noteContentPropertiesDialogOptions, notePropertiesDialogOptions, shareNoteDialogOptions, shareFolderDialogOptions, promptOptions,
|
||||
} = dialogState;
|
||||
|
||||
|
||||
return <>
|
||||
@@ -173,6 +176,8 @@ const WindowCommandsAndDialogs: React.FC<Props> = props => {
|
||||
buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null}
|
||||
inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null}
|
||||
/>
|
||||
|
||||
<PopupNotificationList/>
|
||||
</>;
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { GotoAnythingUserData, Mode, UserDataCallbackReject, UserDataCallbackResolve } from '../../../plugins/GotoAnything';
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
|
||||
export enum UiType {
|
||||
@@ -8,6 +9,10 @@ export enum UiType {
|
||||
ControlledApi = 'controlledApi',
|
||||
}
|
||||
|
||||
export interface GotoAnythingOptions {
|
||||
mode?: Mode;
|
||||
}
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'gotoAnything',
|
||||
label: () => _('Goto Anything...'),
|
||||
@@ -24,19 +29,26 @@ function menuItemById(id: string) {
|
||||
// calling the click() handler.
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, uiType: UiType = UiType.GotoAnything) => {
|
||||
execute: async (_context: CommandContext, uiType: UiType = UiType.GotoAnything, options: GotoAnythingOptions = null) => {
|
||||
options = {
|
||||
mode: Mode.Default,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (uiType === UiType.GotoAnything) {
|
||||
menuItemById('gotoAnything').click();
|
||||
} else if (uiType === UiType.CommandPalette) {
|
||||
menuItemById('commandPalette').click();
|
||||
} else if (uiType === UiType.ControlledApi) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
return new Promise((resolve: Function, reject: Function) => {
|
||||
return new Promise((resolve: UserDataCallbackResolve, reject: UserDataCallbackReject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const menuItem = PluginManager.instance().menuItems().find((i: any) => i.id === 'controlledApi');
|
||||
menuItem.userData = {
|
||||
const userData: GotoAnythingUserData = {
|
||||
callback: { resolve, reject },
|
||||
mode: options.mode,
|
||||
};
|
||||
menuItem.userData = userData;
|
||||
menuItem.click();
|
||||
});
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import * as exportPdf from './exportPdf';
|
||||
import * as gotoAnything from './gotoAnything';
|
||||
import * as hideModalMessage from './hideModalMessage';
|
||||
import * as leaveSharedFolder from './leaveSharedFolder';
|
||||
import * as linkToNote from './linkToNote';
|
||||
import * as moveToFolder from './moveToFolder';
|
||||
import * as newFolder from './newFolder';
|
||||
import * as newNote from './newNote';
|
||||
@@ -28,7 +29,6 @@ import * as restoreNote from './restoreNote';
|
||||
import * as revealResourceFile from './revealResourceFile';
|
||||
import * as search from './search';
|
||||
import * as setTags from './setTags';
|
||||
import * as showEditorPlugin from './showEditorPlugin';
|
||||
import * as showModalMessage from './showModalMessage';
|
||||
import * as showNoteContentProperties from './showNoteContentProperties';
|
||||
import * as showNoteProperties from './showNoteProperties';
|
||||
@@ -36,7 +36,6 @@ import * as showPrompt from './showPrompt';
|
||||
import * as showShareFolderDialog from './showShareFolderDialog';
|
||||
import * as showShareNoteDialog from './showShareNoteDialog';
|
||||
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
||||
import * as toggleEditorPlugin from './toggleEditorPlugin';
|
||||
import * as toggleEditors from './toggleEditors';
|
||||
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
||||
import * as toggleMenuBar from './toggleMenuBar';
|
||||
@@ -58,6 +57,7 @@ const index: any[] = [
|
||||
gotoAnything,
|
||||
hideModalMessage,
|
||||
leaveSharedFolder,
|
||||
linkToNote,
|
||||
moveToFolder,
|
||||
newFolder,
|
||||
newNote,
|
||||
@@ -78,7 +78,6 @@ const index: any[] = [
|
||||
revealResourceFile,
|
||||
search,
|
||||
setTags,
|
||||
showEditorPlugin,
|
||||
showModalMessage,
|
||||
showNoteContentProperties,
|
||||
showNoteProperties,
|
||||
@@ -86,7 +85,6 @@ const index: any[] = [
|
||||
showShareFolderDialog,
|
||||
showShareNoteDialog,
|
||||
showSpellCheckerMenu,
|
||||
toggleEditorPlugin,
|
||||
toggleEditors,
|
||||
toggleLayoutMoveMode,
|
||||
toggleMenuBar,
|
||||
|
@@ -0,0 +1,37 @@
|
||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Mode } from '../../../plugins/GotoAnything';
|
||||
import { GotoAnythingOptions, UiType } from './gotoAnything';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import markdownUtils from '@joplin/lib/markdownUtils';
|
||||
|
||||
const logger = Logger.create('linkToNote');
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'linkToNote',
|
||||
label: () => _('Link to note...'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
const options: GotoAnythingOptions = {
|
||||
mode: Mode.TitleOnly,
|
||||
};
|
||||
const result = await CommandService.instance().execute('gotoAnything', UiType.ControlledApi, options);
|
||||
if (!result) return result;
|
||||
|
||||
if (result.type !== ModelType.Note) {
|
||||
logger.warn('Retrieved item is not a note:', result);
|
||||
return null;
|
||||
}
|
||||
|
||||
const link = `[${markdownUtils.escapeTitleText(result.item.title)}](:/${markdownUtils.escapeLinkUrl(result.item.id)})`;
|
||||
await CommandService.instance().execute('insertText', link);
|
||||
return result;
|
||||
},
|
||||
|
||||
enabledCondition: 'markdownEditorPaneVisible || richTextEditorVisible',
|
||||
};
|
||||
};
|
@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
import { WindowControl } from '../utils/useWindowControl';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleNoteList',
|
||||
@@ -10,14 +11,16 @@ export const declaration: CommandDeclaration = {
|
||||
iconName: 'fas fa-align-justify',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
const layout = (context.state as AppState).mainLayout;
|
||||
|
||||
const visible = !layoutItemProp(layout, 'noteList', 'visible');
|
||||
const newLayout = setLayoutItemProps(layout, 'noteList', {
|
||||
visible: !layoutItemProp(layout, 'noteList', 'visible'),
|
||||
visible,
|
||||
});
|
||||
control.announcePanelVisibility(_('Note list'), visible);
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
|
@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
import { WindowControl } from '../utils/useWindowControl';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleSideBar',
|
||||
@@ -10,14 +11,16 @@ export const declaration: CommandDeclaration = {
|
||||
iconName: 'fas fa-bars',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
const layout = (context.state as AppState).mainLayout;
|
||||
|
||||
const visible = !layoutItemProp(layout, 'sideBar', 'visible');
|
||||
const newLayout = setLayoutItemProps(layout, 'sideBar', {
|
||||
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
|
||||
visible,
|
||||
});
|
||||
control.announcePanelVisibility(_('Sidebar'), visible);
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
|
@@ -2,10 +2,13 @@ import * as React from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { DialogState } from '../types';
|
||||
import { PrintCallback } from './usePrintToCallback';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import announceForAccessibility from '../../utils/announceForAccessibility';
|
||||
|
||||
export interface WindowControl {
|
||||
setState: (update: Partial<DialogState>)=> void;
|
||||
printTo: PrintCallback;
|
||||
announcePanelVisibility(panelName: string, visible: boolean): void;
|
||||
}
|
||||
|
||||
export type OnSetDialogState = React.Dispatch<React.SetStateAction<DialogState>>;
|
||||
@@ -24,6 +27,11 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
|
||||
}));
|
||||
},
|
||||
printTo: (target, options) => onPrintRef.current(target, options),
|
||||
announcePanelVisibility: (panelName, visible) => {
|
||||
announceForAccessibility(
|
||||
visible ? _('Panel "%s" is visible', panelName) : _('Panel %s is hidden', panelName),
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [setDialogState]);
|
||||
};
|
||||
|
@@ -33,6 +33,10 @@ export const SearchInput = styled(StyledInput)`
|
||||
padding-right: 20px;
|
||||
flex: 1;
|
||||
width: 10px;
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@@ -4,6 +4,7 @@ export default function() {
|
||||
'copyDevCommand',
|
||||
'exportPdf',
|
||||
'focusElementNoteBody',
|
||||
'focusElementNoteViewer',
|
||||
'focusElementNoteList',
|
||||
'focusElementNoteTitle',
|
||||
'focusElementSideBar',
|
||||
@@ -43,9 +44,11 @@ export default function() {
|
||||
'togglePerFolderSortOrder',
|
||||
'toggleSideBar',
|
||||
'toggleVisiblePanes',
|
||||
'toggleEditorPlugin',
|
||||
'toggleTabMovesFocus',
|
||||
'editor.deleteLine',
|
||||
'editor.duplicateLine',
|
||||
'newAppInstance',
|
||||
// We cannot put the undo/redo commands in the menu because they are
|
||||
// editor-specific commands. If we put them there it will break the
|
||||
// undo/redo in regular text fields.
|
||||
@@ -59,6 +62,7 @@ export default function() {
|
||||
'editor.sortSelectedLines',
|
||||
'editor.swapLineUp',
|
||||
'editor.swapLineDown',
|
||||
'linkToNote',
|
||||
'exportDeletionLog',
|
||||
'toggleSafeMode',
|
||||
'showShareNoteDialog',
|
||||
|
@@ -139,11 +139,8 @@
|
||||
const viewerPercent = scrollmap.translateL2V(percent);
|
||||
const newScrollTop = viewerPercent * maxScrollTop();
|
||||
|
||||
// Even if the scroll position hasn't changed (percent is the same),
|
||||
// we still ignore the next scroll event, so that it doesn't create
|
||||
// undesired side effects.
|
||||
// https://github.com/laurent22/joplin/issues/7617
|
||||
ignoreNextScrollEvent();
|
||||
// The next scroll event cannot be skipped in order to correctly
|
||||
// scroll to the target section in a different note when follwing a link
|
||||
|
||||
if (Math.floor(contentElement.scrollTop) !== Math.floor(newScrollTop)) {
|
||||
percentScroll_ = percent;
|
||||
@@ -377,6 +374,53 @@
|
||||
contentElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
const getLineCorrespondingTo = (editorLineNumber) => {
|
||||
const lineElements = document.getElementsByClassName('maps-to-line');
|
||||
let lastLineElement;
|
||||
let lastLine = 0;
|
||||
for (const element of lineElements) {
|
||||
// Stop just before the element that corresponds to a greater position
|
||||
if (Number(element.getAttribute('source-line')) > editorLineNumber) {
|
||||
break;
|
||||
}
|
||||
lastLineElement = element;
|
||||
}
|
||||
return lastLineElement;
|
||||
};
|
||||
|
||||
const makeTemporarilyFocusable = (element) => {
|
||||
const dataOriginalTabIndexAttr = 'data-original-tabindex';
|
||||
const originalTabIndex = (
|
||||
element.getAttribute(dataOriginalTabIndexAttr) ?? element.getAttribute('tabindex')
|
||||
);
|
||||
element.setAttribute(dataOriginalTabIndexAttr, originalTabIndex);
|
||||
element.setAttribute('tabindex', '0');
|
||||
|
||||
return {
|
||||
reset: () => {
|
||||
element.setAttribute('tabindex', originalTabIndex);
|
||||
element.removeAttribute(dataOriginalTabIndexAttr);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
ipc.focusLine = (event) => {
|
||||
const targetLine = event.line;
|
||||
const lineElement = getLineCorrespondingTo(targetLine);
|
||||
if (lineElement) {
|
||||
// To allow focusing, the element needs to briefly have tabindex=0.
|
||||
const { reset } = makeTemporarilyFocusable(lineElement);
|
||||
|
||||
lineElement.focus({ preventScroll: true });
|
||||
|
||||
// Reset the tabindex after the browser has had time to focus the element.
|
||||
// When a screen reader is enabled, focus stays on the lineElement.
|
||||
setTimeout(() => {
|
||||
reset();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
const rewriteFileUrls = (accessKey) => {
|
||||
if (!accessKey) return;
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
@use './user-webview-dialog.scss';
|
||||
@use './prompt-dialog.scss';
|
||||
@use './flat-button.scss';
|
||||
@use './link-button.scss';
|
||||
@use './help-text.scss';
|
||||
@use './toolbar-button.scss';
|
||||
@use './toolbar-icon.scss';
|
||||
@@ -14,3 +15,5 @@
|
||||
@use './combobox-wrapper.scss';
|
||||
@use './combobox-suggestion-option.scss';
|
||||
@use './change-app-layout-dialog.scss';
|
||||
@use './popup-notification-list.scss';
|
||||
@use './popup-notification-item.scss';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user