Compare commits
182 Commits
android-v3
...
android-v3
Author | SHA1 | Date | |
---|---|---|---|
|
aa23d5cdff | ||
|
a97c04c21c | ||
|
9f66d7cfcd | ||
|
a52b206dfb | ||
|
806377e6ee | ||
|
6ce55a5737 | ||
|
cd40861ec8 | ||
|
a1aa4f78c9 | ||
|
0caecedb8f | ||
|
af7e172438 | ||
|
1f45252fc6 | ||
|
d2ae02d066 | ||
|
6d98f8102d | ||
|
08eab7a73a | ||
|
8d8c91ef50 | ||
|
88b3c7f526 | ||
|
24731edf92 | ||
|
85557b6882 | ||
|
8dfc873ceb | ||
|
8961aebc3a | ||
|
500c31565d | ||
|
223a685529 | ||
|
135d46f31b | ||
|
9f997c2fb6 | ||
|
1c2c071952 | ||
|
08348c88eb | ||
|
70bfb9f18d | ||
|
292d2fbc15 | ||
|
88cf1d6232 | ||
|
9cf298ef44 | ||
|
19af6a8722 | ||
|
5c8be448ab | ||
|
f69dffcf23 | ||
|
88271bf1a7 | ||
|
14cc053094 | ||
|
65ef700fdc | ||
|
9dbd481f28 | ||
|
e5c8b4bb66 | ||
|
ff6d700499 | ||
|
fc1699ac91 | ||
|
596bcd8d8b | ||
|
ecc4f3e22a | ||
|
c0dc30d0c4 | ||
|
f02af3af3b | ||
|
8179d3e723 | ||
|
06264847cc | ||
|
3137d5be33 | ||
|
d4c35b8c0b | ||
|
39ad1e23a8 | ||
|
d6dd23e921 | ||
|
b108bf799d | ||
|
6d92e982dc | ||
|
40bd2dfe21 | ||
|
819de1cfa4 | ||
|
a6d6e70b3d | ||
|
05cf51ec65 | ||
|
0935b6f697 | ||
|
dd5240d018 | ||
|
3fbb3b6b82 | ||
|
77b74daa0e | ||
|
8c0769fdb3 | ||
|
d2028588e8 | ||
|
4b99c2062c | ||
|
ce0218700e | ||
|
d63f498f4c | ||
|
56d2aced8a | ||
|
db2a194b69 | ||
|
f7a970f466 | ||
|
f7fa7a195f | ||
|
e6ec27a501 | ||
|
331f7ebe5c | ||
|
afcd2d2a39 | ||
|
8129f4a89f | ||
|
72c1bb3724 | ||
|
8fdccd287e | ||
|
b69a7403bc | ||
|
bdc9fa9dc3 | ||
|
9c07e57e28 | ||
|
821daeca94 | ||
|
480bf238f6 | ||
|
8ff13e5fc4 | ||
|
8e1970d08e | ||
|
86d92dd302 | ||
|
71b466507f | ||
|
11ce5f6c52 | ||
|
630b4061f0 | ||
|
912c943114 | ||
|
8e377e0306 | ||
|
1535e020a3 | ||
|
23d5d3426d | ||
|
6ab7a0836e | ||
|
278691211d | ||
|
356d4688a0 | ||
|
6b1d31387b | ||
|
70bfb26c9a | ||
|
71f70f4d2c | ||
|
64e4ebb1f3 | ||
|
2d984ce9a8 | ||
|
eaf160e0b1 | ||
|
624bfd9175 | ||
|
9ad1249f11 | ||
|
668849603d | ||
|
24f4c8e6ab | ||
|
46f5784edc | ||
|
fae2443481 | ||
|
37d65e000a | ||
|
6dd90eb03f | ||
|
3d8f713eb7 | ||
|
c35efe15d2 | ||
|
1596b46b86 | ||
|
4de0236194 | ||
|
2ab9702e32 | ||
|
24954bd0f0 | ||
|
2d4322be56 | ||
|
abb069bf50 | ||
|
a81d9fe17a | ||
|
6d44158050 | ||
|
a63cf3a90d | ||
|
ddb4f8c45b | ||
|
d7adab59ef | ||
|
e41374496e | ||
|
62d514463c | ||
|
332078b4ea | ||
|
c60e11646d | ||
|
c607fe9c75 | ||
|
1a4ba2c74a | ||
|
e49bca8315 | ||
|
636fbdf7d0 | ||
|
ee97434bb0 | ||
|
599cf5b86f | ||
|
2fd6a3a2fa | ||
|
a3e04103de | ||
|
731260926d | ||
|
a43635610a | ||
|
e307459652 | ||
|
c197a83de8 | ||
|
320d0df60d | ||
|
7e4533d811 | ||
|
f32fe63205 | ||
|
be117bca86 | ||
|
2b7bd902f3 | ||
|
3e0fb48e44 | ||
|
6d7fd19167 | ||
|
c3520d9eb1 | ||
|
5fd3cecc96 | ||
|
0d8666c946 | ||
|
4a475f1b53 | ||
|
8679cc5704 | ||
|
a48c4ba93f | ||
|
12db667128 | ||
|
6215de6080 | ||
|
7d2f384475 | ||
|
6ea1ac09a4 | ||
|
f2841a9a94 | ||
|
46ade2e0f8 | ||
|
d89be23069 | ||
|
337d50437b | ||
|
2479a8471e | ||
|
16e82b5462 | ||
|
6c091910cd | ||
|
a074532497 | ||
|
5d2df358ac | ||
|
dfdc2fda27 | ||
|
a1f9c9c3d8 | ||
|
838da6f161 | ||
|
a86ee1d34e | ||
|
5f34a1bc92 | ||
|
f781face3a | ||
|
78ecd28d73 | ||
|
85e57a3953 | ||
|
95968f6690 | ||
|
f0b73ee916 | ||
|
a44412ae78 | ||
|
c7116b135f | ||
|
801d36c41f | ||
|
1d46adf801 | ||
|
94edaea210 | ||
|
5db88995c0 | ||
|
8eda8d3c84 | ||
|
1437dd5f27 | ||
|
9eb4944614 | ||
|
b4ef5abb88 |
124
.eslintignore
@@ -51,8 +51,10 @@ packages/app-desktop/node_modules
|
||||
packages/app-desktop/packageInfo.js
|
||||
packages/app-desktop/services/electron-context-menu.js
|
||||
packages/app-desktop/vendor/lib/
|
||||
packages/app-mobile/packageInfo.js
|
||||
packages/app-mobile/android
|
||||
packages/app-mobile/**/*.bundle.js
|
||||
packages/app-mobile/web/public/pluginAssets/**/*
|
||||
packages/app-mobile/ios
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/locales
|
||||
@@ -167,9 +169,13 @@ packages/app-desktop/gui/Button/Button.js
|
||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
@@ -296,6 +302,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
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/index.js
|
||||
@@ -432,6 +440,7 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
@@ -451,24 +460,30 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/dragAndDrop.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gulpfile.js
|
||||
packages/app-desktop/integration-tests/goToAnything.spec.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/settings.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-desktop/services/commands/types.js
|
||||
@@ -500,6 +515,10 @@ packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
@@ -513,19 +532,24 @@ packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
@@ -582,20 +606,44 @@ packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.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/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/components/plugins/types.js
|
||||
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
@@ -646,6 +694,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -660,44 +709,22 @@ packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
@@ -706,9 +733,13 @@ packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
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/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
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver/tarCreate.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.test.js
|
||||
@@ -717,17 +748,29 @@ 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/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/shim-init-react/index.js
|
||||
packages/app-mobile/utils/shim-init-react/index.web.js
|
||||
packages/app-mobile/utils/shim-init-react/injectedJs.js
|
||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
packages/default-plugins/buildDefaultPlugins.js
|
||||
packages/default-plugins/commands/buildAll.js
|
||||
@@ -784,6 +827,7 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
@@ -798,6 +842,7 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
@@ -898,6 +943,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
@@ -944,8 +990,10 @@ packages/lib/models/Tag.test.js
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
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/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -1004,6 +1052,7 @@ packages/lib/services/commands/commandsToMarkdownTable.js
|
||||
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
||||
packages/lib/services/commands/isEditorCommand.js
|
||||
packages/lib/services/commands/propsHaveChanged.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.test.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.js
|
||||
packages/lib/services/contextkey/contextkey.js
|
||||
packages/lib/services/database/addMigrationFile.js
|
||||
@@ -1055,8 +1104,10 @@ packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.test.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.electron.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
@@ -1110,7 +1161,11 @@ packages/lib/services/plugins/api/noteListType.js
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/loadPlugins.test.js
|
||||
packages/lib/services/plugins/loadPlugins.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
@@ -1252,13 +1307,17 @@ packages/lib/urlUtils.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/dom/makeSandboxedIframe.js
|
||||
packages/lib/utils/focusHandler.js
|
||||
packages/lib/utils/frontMatter.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
packages/lib/utils/ipc/WindowMessenger.js
|
||||
packages/lib/utils/ipc/WorkerMessenger.js
|
||||
packages/lib/utils/ipc/WorkerToWindowMessenger.js
|
||||
packages/lib/utils/ipc/types.js
|
||||
packages/lib/utils/ipc/utils/isTransferableObject.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
@@ -1269,6 +1328,8 @@ packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
packages/lib/utils/resolvePathWithinDir.test.js
|
||||
packages/lib/utils/resolvePathWithinDir.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
@@ -1386,6 +1447,7 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
packages/tools/website/processDocs.test.js
|
||||
|
13
.eslintrc.js
@@ -15,6 +15,19 @@ module.exports = {
|
||||
'globals': {
|
||||
'Atomics': 'readonly',
|
||||
'SharedArrayBuffer': 'readonly',
|
||||
'BufferEncoding': 'readonly',
|
||||
'AsyncIterable': 'readonly',
|
||||
'FileSystemFileHandle': 'readonly',
|
||||
'FileSystemDirectoryHandle': 'readonly',
|
||||
'ReadableStreamDefaultReader': 'readonly',
|
||||
'FileSystemCreateWritableOptions': 'readonly',
|
||||
'FileSystemHandle': 'readonly',
|
||||
'IDBTransactionMode': 'readonly',
|
||||
|
||||
// ServiceWorker
|
||||
'ExtendableEvent': 'readonly',
|
||||
'WindowClient': 'readonly',
|
||||
'FetchEvent': 'readonly',
|
||||
|
||||
// Jest variables
|
||||
'test': 'readonly',
|
||||
|
17
.github/workflows/automerge.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: automerge
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- id: automerge
|
||||
name: automerge
|
||||
uses: "pascalgn/automerge-action@v0.16.3"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: "squash"
|
||||
LOG: "DEBUG"
|
122
.gitignore
vendored
@@ -146,9 +146,13 @@ packages/app-desktop/gui/Button/Button.js
|
||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
@@ -275,6 +279,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
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/index.js
|
||||
@@ -411,6 +417,7 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
@@ -430,24 +437,30 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/dragAndDrop.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gulpfile.js
|
||||
packages/app-desktop/integration-tests/goToAnything.spec.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/settings.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-desktop/services/commands/types.js
|
||||
@@ -479,6 +492,10 @@ packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
@@ -492,19 +509,24 @@ packages/app-mobile/commands/openItem.js
|
||||
packages/app-mobile/commands/openNote.js
|
||||
packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
@@ -561,20 +583,44 @@ packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.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/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/components/plugins/types.js
|
||||
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
@@ -625,6 +671,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -639,44 +686,22 @@ packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/index.web.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/tools/copyAssets.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
@@ -685,9 +710,13 @@ packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
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/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
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver/tarCreate.js
|
||||
packages/app-mobile/utils/fs-driver/tarExtract.test.js
|
||||
@@ -696,17 +725,29 @@ 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/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/shim-init-react/index.js
|
||||
packages/app-mobile/utils/shim-init-react/index.web.js
|
||||
packages/app-mobile/utils/shim-init-react/injectedJs.js
|
||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
packages/default-plugins/buildDefaultPlugins.js
|
||||
packages/default-plugins/commands/buildAll.js
|
||||
@@ -763,6 +804,7 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
@@ -777,6 +819,7 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
@@ -877,6 +920,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
@@ -923,8 +967,10 @@ packages/lib/models/Tag.test.js
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
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/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -983,6 +1029,7 @@ packages/lib/services/commands/commandsToMarkdownTable.js
|
||||
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
||||
packages/lib/services/commands/isEditorCommand.js
|
||||
packages/lib/services/commands/propsHaveChanged.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.test.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.js
|
||||
packages/lib/services/contextkey/contextkey.js
|
||||
packages/lib/services/database/addMigrationFile.js
|
||||
@@ -1034,8 +1081,10 @@ packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.test.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.electron.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
@@ -1089,7 +1138,11 @@ packages/lib/services/plugins/api/noteListType.js
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/loadPlugins.test.js
|
||||
packages/lib/services/plugins/loadPlugins.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
@@ -1231,13 +1284,17 @@ packages/lib/urlUtils.js
|
||||
packages/lib/utils/ActionLogger.test.js
|
||||
packages/lib/utils/ActionLogger.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/dom/makeSandboxedIframe.js
|
||||
packages/lib/utils/focusHandler.js
|
||||
packages/lib/utils/frontMatter.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.test.js
|
||||
packages/lib/utils/ipc/RemoteMessenger.js
|
||||
packages/lib/utils/ipc/TestMessenger.js
|
||||
packages/lib/utils/ipc/WindowMessenger.js
|
||||
packages/lib/utils/ipc/WorkerMessenger.js
|
||||
packages/lib/utils/ipc/WorkerToWindowMessenger.js
|
||||
packages/lib/utils/ipc/types.js
|
||||
packages/lib/utils/ipc/utils/isTransferableObject.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
|
||||
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
|
||||
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
|
||||
@@ -1248,6 +1305,8 @@ packages/lib/utils/joplinCloud/types.js
|
||||
packages/lib/utils/processStartFlags.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.test.js
|
||||
packages/lib/utils/replaceUnsupportedCharacters.js
|
||||
packages/lib/utils/resolvePathWithinDir.test.js
|
||||
packages/lib/utils/resolvePathWithinDir.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
@@ -1365,6 +1424,7 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
packages/tools/website/processDocs.test.js
|
||||
|
118
.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch
Normal file
@@ -0,0 +1,118 @@
|
||||
# Fixes sync issues caused by locale-sensitive lowercasing
|
||||
# of HTTP headers.
|
||||
# See https://github.com/laurent22/joplin/issues/10681
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
index 8ac9e7a855162cefbf99024eb013c8a3b11de1ec..1c639cf9d84821b6ffc132960e2d1c044bedbd48 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
@@ -2,6 +2,7 @@ package com.RNFetchBlob;
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
+import java.util.Locale;
|
||||
|
||||
class RNFetchBlobConfig {
|
||||
|
||||
@@ -33,7 +34,7 @@ class RNFetchBlobConfig {
|
||||
}
|
||||
if(options.hasKey("binaryContentTypes"))
|
||||
this.binaryContentTypes = options.getArray("binaryContentTypes");
|
||||
- if(this.path != null && path.toLowerCase().contains("?append=true")) {
|
||||
+ if(this.path != null && path.toLowerCase(Locale.ROOT).contains("?append=true")) {
|
||||
this.overwrite = false;
|
||||
}
|
||||
if(options.hasKey("overwrite"))
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
index a4d70153f41e6c14eec65412b5b59822f1c6750b..d98c439f7b0aeb79afc82ab9f653e9c021086426 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
@@ -29,6 +29,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
+import java.util.Locale;
|
||||
|
||||
class RNFetchBlobFS {
|
||||
|
||||
@@ -210,7 +211,7 @@ class RNFetchBlobFS {
|
||||
return;
|
||||
}
|
||||
|
||||
- switch (encoding.toLowerCase()) {
|
||||
+ switch (encoding.toLowerCase(Locale.ROOT)) {
|
||||
case "base64" :
|
||||
promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
|
||||
break;
|
||||
@@ -1050,7 +1051,7 @@ class RNFetchBlobFS {
|
||||
if(encoding.equalsIgnoreCase("ascii")) {
|
||||
return data.getBytes(Charset.forName("US-ASCII"));
|
||||
}
|
||||
- else if(encoding.toLowerCase().contains("base64")) {
|
||||
+ else if(encoding.toLowerCase(Locale.ROOT).contains("base64")) {
|
||||
return Base64.decode(data, Base64.NO_WRAP);
|
||||
|
||||
}
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
index a8abd71833879201e3438b2fa51d712a311c4551..b70cc13c004229f69157de5f82ae5ec3abf4358e 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
@@ -49,6 +49,7 @@ import java.security.KeyStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
+import java.util.Locale;
|
||||
import java.util.HashMap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -300,14 +301,14 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
responseFormat = ResponseFormat.UTF8;
|
||||
}
|
||||
else {
|
||||
- builder.header(key.toLowerCase(), value);
|
||||
- mheaders.put(key.toLowerCase(), value);
|
||||
+ builder.header(key.toLowerCase(Locale.ROOT), value);
|
||||
+ mheaders.put(key.toLowerCase(Locale.ROOT), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) {
|
||||
- String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase();
|
||||
+ String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(Locale.ROOT);
|
||||
|
||||
if(rawRequestBodyArray != null) {
|
||||
requestType = RequestType.Form;
|
||||
@@ -323,7 +324,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
|| rawRequestBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
|
||||
requestType = RequestType.SingleFile;
|
||||
}
|
||||
- else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) {
|
||||
+ else if (cType.toLowerCase(Locale.ROOT).contains(";base64") || cType.toLowerCase(Locale.ROOT).startsWith("application/octet")) {
|
||||
cType = cType.replace(";base64","").replace(";BASE64","");
|
||||
if(mheaders.containsKey("content-type"))
|
||||
mheaders.put("content-type", cType);
|
||||
@@ -686,7 +687,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
boolean isCustomBinary = false;
|
||||
if(options.binaryContentTypes != null) {
|
||||
for(int i = 0; i< options.binaryContentTypes.size();i++) {
|
||||
- if(ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) {
|
||||
+ if(ctype.toLowerCase(Locale.ROOT).contains(options.binaryContentTypes.getString(i).toLowerCase(Locale.ROOT))) {
|
||||
isCustomBinary = true;
|
||||
break;
|
||||
}
|
||||
@@ -698,13 +699,13 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
private String getHeaderIgnoreCases(Headers headers, String field) {
|
||||
String val = headers.get(field);
|
||||
if(val != null) return val;
|
||||
- return headers.get(field.toLowerCase()) == null ? "" : headers.get(field.toLowerCase());
|
||||
+ return headers.get(field.toLowerCase(Locale.ROOT)) == null ? "" : headers.get(field.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private String getHeaderIgnoreCases(HashMap<String,String> headers, String field) {
|
||||
String val = headers.get(field);
|
||||
if(val != null) return val;
|
||||
- String lowerCasedValue = headers.get(field.toLowerCase());
|
||||
+ String lowerCasedValue = headers.get(field.toLowerCase(Locale.ROOT));
|
||||
return lowerCasedValue == null ? "" : lowerCasedValue;
|
||||
}
|
||||
|
BIN
Assets/WebsiteAssets/images/desktop-set-alarm.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-mfa.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-mobile-plugins.png
Normal file
After Width: | Height: | Size: 337 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-note-list-multi.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-ocr-data.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-trash.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/BYTV.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Route4Me.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Stormlikes.png
Normal file
After Width: | Height: | Size: 20 KiB |
@@ -1,4 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Fri, 01 Mar 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 01 Jul 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.0]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Trash folder<a name="trash-folder" href="#trash-folder" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin now support a trash folder - any deleted notes or notebooks will be moved to that folder. You can also choose to have these notes permanently deleted after a number of days.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-trash.png" alt=""></p>
|
||||
<p>Support for the trash folder has a somewhat long history in Joplin since it's an obvious and important feature to add, yet it can be particularly tricky once you start realising how many parts of the app it's going to impact.</p>
|
||||
<p>Many attempts have been made over time: my first attempt was based on the note history feature. Indeed since this feature already saves versions of notes, it seems to make sense to use it for the trash feature, and indeed the note history feature <a href="https://joplinapp.org/news/20190523-221026">was designed for this originally</a>. However that approach turned to be needlessly complicated and after modifying hundreds of files just for this, the idea was dropped.</p>
|
||||
<p>The next one was based on using a <a href="https://github.com/laurent22/joplin/issues/483">special "trash" tag</a> - deleted notes would have this tag attached to them and would appear in a special "trash" folder. This approach also had <a href="https://github.com/laurent22/joplin/issues/483">many issues</a> probably the main one being that notebooks can't be tagged, which means we would have to add support for tagged notebooks and that in itself would also be a massive change.</p>
|
||||
<p><a href="https://discourse.joplinapp.org/t/trashcan/3998/16">Various</a>, <a href="https://discourse.joplinapp.org/t/poll-trash-bin-plugin/19951">ideas,</a> were also attempted using plugins, by creating a special "trash folder", but in the end no such plugin was ever created, probably due to limitations of the plugin API.</p>
|
||||
<p>In the end, turned out that this <a href="https://github.com/laurent22/joplin/issues/483#issuecomment-585655742">old idea</a> of adding a "deleted" property to each note and notebook was the easiest approach. With this it was simpler to get to a working solution relatively quickly, and then it was a matter of ensuring that deleted notes don't appear where they shouldn't, such as search results, etc.</p>
|
||||
<h3>Joplin Cloud multi-factor authentication<a name="joplin-cloud-multi-factor-authentication" href="#joplin-cloud-multi-factor-authentication" class="heading-anchor">🔗</a></h3>
|
||||
<p>Multi-factor authentication (MFA), also known as two-factor authentication (2FA) is a security process that requires you to provide two or more verification factors to gain access to a system or account. It typically includes something you know (password), something you have (security token), and something you are (biometric verification).</p>
|
||||
<p>To better secure your account, Joplin Cloud and all Joplin applications now support MFA. To enable it, go to your Joplin Cloud profile, click on "Enable multi-factor authentication" and follow the instructions. Please note that all your applications will then be disconnected, so you will need to login again (your data of course will remain on the app so you won't have to download it again).</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mfa.png" alt=""></p>
|
||||
<h3>Note list with multiple columns<a name="note-list-with-multiple-columns" href="#note-list-with-multiple-columns" class="heading-anchor">🔗</a></h3>
|
||||
<p>In this release we add support for multiple columns in the note list. You can display various properties of the notes, as well as sort the notes by these properties. As usual this feature can be controlled and customised by plugins so for example it should be possible to display custom columns, and display custom information including thumbnails.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-note-list-multi.png" alt=""></p>
|
||||
<h3>Plugin API enhancement<a name="plugin-api-enhancement" href="#plugin-api-enhancement" class="heading-anchor">🔗</a></h3>
|
||||
<p>The plugin API has received several updates to facilitate easy customisation of the app As mentioned above, it is now possible to customise the new note list. Besides this, we've added support for loading PDFs and creating images from them, which can for example be used to create thumbnails.</p>
|
||||
<p>Many other small enhancements have been made to the plugin API to help you tailor the app to your needs!</p>
|
||||
<h3>View OCR data<a name="view-ocr-data" href="#view-ocr-data" class="heading-anchor">🔗</a></h3>
|
||||
<p>Now when you right-click on an image or PDF you have an option to view the OCR (Optical character recognition) data associated with it. That will allow you for example to easily copy and paste the text.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-ocr-data.png" alt=""></p>
|
||||
<h2>Plugin support on mobile<a name="plugin-support-on-mobile" href="#plugin-support-on-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<p>As always, most of the above changes also apply to mobile (iOS and Android), for example the trash folder and MFA support.</p>
|
||||
<p>Additionally the mobile application now adds support for plugins. To enable the feature, go to the settings then to the "Plugins" section. The feature is currently in Beta, in particular it means that some plugins do not work or only partially work. Normally the app should not offer you to install a non-working plugin but that may still happen. In general if you notice any issue with this beta feature please let me us know as we're keen to improve it.</p>
|
||||
<p>Support for cross-platform plugins in Joplin is great news as it means a lot of new features become available on mobile. As of now, we have checked the following plugins and can confirm that they work on mobile:</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.quick-links/">Quick Links</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.inline-tags/">Inline Tags</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/io.github.personalizedrefrigerator.codemirror6-settings/">CodeMirror 6 settings</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.hieuthi.joplin.function-plot/">Function plot</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.space-indenter/">Space indenter</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.alondmnt.tag-navigator/">Inline Tag Navigator</a></li>
|
||||
</ul>
|
||||
<p>Those are just some examples - many more are working!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mobile-plugins.png" alt=""></p>
|
||||
<h1>Full changelogs<a name="full-changelogs" href="#full-changelogs" class="heading-anchor">🔗</a></h1>
|
||||
<p>This is just an overview of the main features. The full changelogs are available there:</p>
|
||||
<ul>
|
||||
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
|
||||
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android/">https://joplinapp.org/help/about/changelog/android/</a></li>
|
||||
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios/">https://joplinapp.org/help/about/changelog/ios/</a></li>
|
||||
</ul>
|
||||
]]></description><link>https://joplinapp.org/news/20240701-release-3-0</link><guid isPermaLink="false">20240701-release-3-0</guid><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 3.0</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
|
||||
<p>Optical Character Recognition (OCR) in Joplin enables the transformation of text-containing images into machine-readable text formats. From this version you can enable OCR in the Configuration screen under the "General" section. Once activated, Joplin scans images and PDFs, extracting text data for searchability.</p>
|
||||
<p>While OCR search is available on both desktop and mobile apps, document scanning is limited to the desktop due to resource demands. For more information head to the <a href="https://joplinapp.org/help/apps/ocr">OCR official documentation</a>!</p>
|
||||
<h2>Bundled plugins<a name="bundled-plugins" href="#bundled-plugins" class="heading-anchor">🔗</a></h2>
|
||||
@@ -394,7 +437,4 @@ sys 0m38.013s</p>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item><item><title><![CDATA[GSoC "Contributor Proposals" phase is starting now!]]></title><description><![CDATA[<p>The "Contributor Proposals" phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to <a href="https://summerofcode.withgoogle.com/">https://summerofcode.withgoogle.com/</a></p>
|
||||
<p>When it's done, please also let us know by posting an update on your forum introduction post.</p>
|
||||
<p>If you haven't created a pull request yet, it's still time to create one. Doing so will greatly increase your chances of being selected!</p>
|
||||
]]></description><link>https://joplinapp.org/news/20220405-gsoc-contributor-proposals</link><guid isPermaLink="false">20220405-gsoc-contributor-proposals</guid><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item></channel></rss>
|
@@ -28,6 +28,7 @@ SILENT=false
|
||||
ALLOW_ROOT=false
|
||||
SHOW_CHANGELOG=false
|
||||
INCLUDE_PRE_RELEASE=false
|
||||
INSTALL_DIR="${HOME}/.joplin" # default installation directory
|
||||
|
||||
print() {
|
||||
if [[ "${SILENT}" == false ]]; then
|
||||
@@ -57,6 +58,7 @@ showHelp() {
|
||||
print "\t" "--force" "\t" "Always download the latest version"
|
||||
print "\t" "--silent" "\t" "Don't print any output"
|
||||
print "\t" "--prerelease" "\t" "Check for new Versions including Pre-Releases"
|
||||
print "\t" "--install-dir" "\t" "Set installation directory; default: \"${INSTALL_DIR}\""
|
||||
|
||||
if [[ ! -z $1 ]]; then
|
||||
print "\n" "${COLOR_RED}ERROR: " "$*" "${COLOR_RESET}" "\n"
|
||||
@@ -84,6 +86,7 @@ while getopts "${optspec}" OPT; do
|
||||
force ) FORCE=true ;;
|
||||
changelog ) SHOW_CHANGELOG=true ;;
|
||||
prerelease ) INCLUDE_PRE_RELEASE=true ;;
|
||||
install-dir ) INSTALL_DIR="$OPTARG" ;;
|
||||
[^\?]* ) showHelp "Illegal option --${OPT}"; exit 2 ;;
|
||||
\? ) showHelp "Illegal option -${OPTARG}"; exit 2 ;;
|
||||
esac
|
||||
@@ -120,9 +123,10 @@ fi
|
||||
print "Checking dependencies..."
|
||||
## Check if libfuse2 is present.
|
||||
if [[ $(command -v ldconfig) ]]; then
|
||||
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
|
||||
else
|
||||
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
|
||||
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]]; then
|
||||
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]]; then
|
||||
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
|
||||
@@ -142,17 +146,17 @@ else
|
||||
fi
|
||||
|
||||
# Check if it's in the latest version
|
||||
if [[ -e ~/.joplin/VERSION ]] && [[ $(< ~/.joplin/VERSION) == "${RELEASE_VERSION}" ]]; then
|
||||
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
|
||||
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
|
||||
else
|
||||
[[ -e ~/.joplin/VERSION ]] && CURRENT_VERSION=$(< ~/.joplin/VERSION)
|
||||
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
|
||||
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
|
||||
fi
|
||||
|
||||
# Check if it's an update or a new install
|
||||
DOWNLOAD_TYPE="New"
|
||||
if [[ -f ~/.joplin/Joplin.AppImage ]]; then
|
||||
if [[ -f "${INSTALL_DIR}/Joplin.AppImage" ]]; then
|
||||
DOWNLOAD_TYPE="Update"
|
||||
fi
|
||||
|
||||
@@ -165,16 +169,16 @@ wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
#-----------------------------------------------------
|
||||
print 'Installing Joplin...'
|
||||
# Delete previous version (in future versions joplin.desktop shouldn't exist)
|
||||
rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/VERSION
|
||||
rm -f "${INSTALL_DIR}"/*.AppImage ~/.local/share/applications/joplin.desktop "${INSTALL_DIR}/VERSION"
|
||||
|
||||
# Creates the folder where the binary will be stored
|
||||
mkdir -p ~/.joplin/
|
||||
mkdir -p "${INSTALL_DIR}/"
|
||||
|
||||
# Download the latest version
|
||||
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
|
||||
mv "${TEMP_DIR}/Joplin.AppImage" "${INSTALL_DIR}/Joplin.AppImage"
|
||||
|
||||
# Gives execution privileges
|
||||
chmod +x ~/.joplin/Joplin.AppImage
|
||||
chmod +x "${INSTALL_DIR}/Joplin.AppImage"
|
||||
|
||||
print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
|
||||
@@ -253,7 +257,7 @@ if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cin
|
||||
Encoding=UTF-8
|
||||
Name=Joplin
|
||||
Comment=Joplin for Desktop
|
||||
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE ${HOME}/.joplin/Joplin.AppImage ${SANDBOXPARAM} %u
|
||||
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" ${SANDBOXPARAM} %u
|
||||
Icon=joplin
|
||||
StartupWMClass=Joplin
|
||||
Type=Application
|
||||
@@ -278,7 +282,7 @@ fi
|
||||
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
|
||||
# Record version
|
||||
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
|
||||
echo "$RELEASE_VERSION" > "${INSTALL_DIR}/VERSION"
|
||||
|
||||
#-----------------------------------------------------
|
||||
if [[ "$SHOW_CHANGELOG" == true ]]; then
|
||||
|
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <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>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
@@ -15,7 +15,6 @@
|
||||
# SLAVE_POSTGRES_USER=joplin
|
||||
# SLAVE_POSTGRES_PORT=5433
|
||||
# SLAVE_POSTGRES_HOST=localhost
|
||||
# USERS_WITH_REPLICATION=ID1,ID2,...
|
||||
|
||||
version: '2'
|
||||
|
||||
|
@@ -21,7 +21,8 @@ module.exports = {
|
||||
// See https://github.com/lint-staged/lint-staged/issues/934#issuecomment-743299357
|
||||
'*.{js,jsx,ts,tsx,task1}': 'yarn checkIgnoredFiles',
|
||||
'*.{js,jsx,ts,tsx,task2}': 'yarn spellcheck',
|
||||
'*.{js,jsx,ts,tsx,task3}': 'yarn packageJsonLint',
|
||||
'*.{js,jsx,ts,tsx,task4}': 'yarn linter-precommit',
|
||||
'*.{md,mdx}': 'yarn spellcheck',
|
||||
'*.{js,jsx,ts,tsx,task3}': 'yarn linter-precommit',
|
||||
'*.{json,task4}': 'yarn packageJsonLint',
|
||||
'*.{md,mdx,task5}': 'yarn spellcheck',
|
||||
'*.{md,mdx,task6}': 'yarn validateFilenames',
|
||||
};
|
||||
|
@@ -59,6 +59,7 @@
|
||||
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
|
||||
"updateNews": "node ./packages/tools/website/updateNews",
|
||||
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
|
||||
"validateFilenames": "node ./packages/tools/validateFilenames.js",
|
||||
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
|
||||
"watchWebsite": "nodemon --delay 1 --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --watch packages/doc-builder/build --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
|
||||
},
|
||||
@@ -82,7 +83,7 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.12",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
@@ -109,6 +110,7 @@
|
||||
"@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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Setting, { SettingStorage } from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType, SettingStorage } from '@joplin/lib/models/Setting';
|
||||
import { SettingItemType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
@@ -61,7 +61,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const description: string[] = [];
|
||||
if (md.label && md.label()) description.push(md.label());
|
||||
if (md.description && md.description('desktop')) description.push(md.description('desktop'));
|
||||
if (md.description && md.description(AppType.Desktop)) description.push(md.description(AppType.Desktop));
|
||||
|
||||
if (description.length) props.description = description.join('. ');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.0",
|
||||
"@joplin/renderer": "~3.0",
|
||||
"@joplin/utils": "~3.0",
|
||||
"@joplin/lib": "~3.1",
|
||||
"@joplin/renderer": "~3.1",
|
||||
"@joplin/utils": "~3.1",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -57,23 +57,23 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.33.2",
|
||||
"sharp": "0.33.3",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"terminal-kit": "3.0.2",
|
||||
"terminal-kit": "3.1.1",
|
||||
"tkwidgets": "0.5.27",
|
||||
"url-parse": "1.5.10",
|
||||
"word-wrap": "1.2.5",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.0",
|
||||
"@joplin/tools": "~3.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/node": "18.19.33",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
@@ -2,13 +2,16 @@ import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { RenderOptions } from '@joplin/renderer/types';
|
||||
import { isResourceUrl, resourceUrlToId } from '@joplin/lib/models/utils/resourceUtils';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function newTestMdToHtml(options: any = null) {
|
||||
options = {
|
||||
ResourceModel: {
|
||||
isResourceUrl: () => false,
|
||||
isResourceUrl: isResourceUrl,
|
||||
urlToId: resourceUrlToId,
|
||||
},
|
||||
fsDriver: shim.fsDriver(),
|
||||
...options,
|
||||
@@ -39,7 +42,7 @@ describe('MdToHtml', () => {
|
||||
// if (mdFilename !== 'sanitize_9.md') continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mdToHtmlOptions: any = {
|
||||
const mdToHtmlOptions: RenderOptions = {
|
||||
bodyOnly: true,
|
||||
};
|
||||
|
||||
@@ -51,6 +54,8 @@ describe('MdToHtml', () => {
|
||||
};
|
||||
} else if (mdFilename.startsWith('sourcemap_')) {
|
||||
mdToHtmlOptions.mapsToLine = true;
|
||||
} else if (mdFilename.startsWith('resource_')) {
|
||||
mdToHtmlOptions.resources = {};
|
||||
}
|
||||
|
||||
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
||||
|
48
packages/app-cli/tests/html_to_md/resource_placeholder.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<p>Markdown images:</p>
|
||||
<ul>
|
||||
<li>
|
||||
With ALT and title:
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-test"
|
||||
data-original-alt="test"
|
||||
data-original-title="testing"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
With neither ALT nor title:
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-alt=""
|
||||
data-original-title=""
|
||||
data-resource-id="0a25d61cc33e57afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>HTML images:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-before=" width="230""
|
||||
data-original-after=" style="border: 32px inset red;"/"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-after="/"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
@@ -0,0 +1,9 @@
|
||||
Markdown images:
|
||||
|
||||
- With ALT and title:
|
||||
- With neither ALT nor title:
|
||||
|
||||
HTML images:
|
||||
|
||||
- <img width="230" src=":/0415d61cc33e47afa6dde45948c3177f" style="border: 32px inset red;"/>
|
||||
- <img src=":/0415d61cc33e47afa6dde45948c3177f" />
|
@@ -0,0 +1,15 @@
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class="jop-noMdConv"/" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
@@ -0,0 +1,3 @@
|
||||

|
||||

|
||||
<img src=":/a1test2a1test2a1test2a1test22347"/>
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
@@ -54,8 +54,9 @@
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["service_worker.mjs"],
|
||||
|
||||
"scripts": [
|
||||
"service_worker.mjs"
|
||||
],
|
||||
"service_worker": "service_worker.mjs",
|
||||
"type": "module"
|
||||
},
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
|
||||
import { PluginMessage } from './services/plugins/PluginRunner';
|
||||
// import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
@@ -13,6 +14,7 @@ const fs = require('fs-extra');
|
||||
import { dialog, ipcMain } from 'electron';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
|
||||
import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
|
||||
interface RendererProcessQuitReply {
|
||||
@@ -40,6 +42,8 @@ export default class ElectronAppWrapper {
|
||||
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
|
||||
private pluginWindows_: PluginWindows = {};
|
||||
private initialCallbackUrl_: string = null;
|
||||
// private updaterService_: AutoUpdaterService = null;
|
||||
private customProtocolHandler_: CustomProtocolHandler = 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) {
|
||||
@@ -454,6 +458,14 @@ export default class ElectronAppWrapper {
|
||||
return false;
|
||||
}
|
||||
|
||||
public initializeCustomProtocolHandler(logger: LoggerWrapper) {
|
||||
this.customProtocolHandler_ ??= handleCustomProtocols(logger);
|
||||
}
|
||||
|
||||
public getCustomProtocolHandler() {
|
||||
return this.customProtocolHandler_;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
// Since we are doing other async things before creating the window, we might miss
|
||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||
@@ -464,6 +476,13 @@ export default class ElectronAppWrapper {
|
||||
|
||||
this.createWindow();
|
||||
|
||||
// TODO: Disabled for now - needs to be behind a feature flag
|
||||
|
||||
// if (!shim.isLinux()) {
|
||||
// this.updaterService_ = new AutoUpdaterService();
|
||||
// this.updaterService_.startPeriodicUpdateCheck();
|
||||
// }
|
||||
|
||||
this.electronApp_.on('before-quit', () => {
|
||||
this.willQuitApp_ = true;
|
||||
});
|
||||
|
@@ -71,6 +71,7 @@ import OcrService from '@joplin/lib/services/ocr/OcrService';
|
||||
import OcrDriverTesseract from '@joplin/lib/services/ocr/drivers/OcrDriverTesseract';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
|
||||
const pluginClasses = [
|
||||
@@ -88,6 +89,7 @@ class Application extends BaseApplication {
|
||||
private checkAllPluginStartedIID_: any = null;
|
||||
private initPluginServiceDone_ = false;
|
||||
private ocrService_: OcrService;
|
||||
private protocolHandler_: CustomProtocolHandler;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
@@ -129,7 +131,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'ocr.enabled' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
this.setupOcrService();
|
||||
void this.setupOcrService();
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
@@ -167,6 +169,12 @@ class Application extends BaseApplication {
|
||||
this.handleThemeAutoDetect();
|
||||
}
|
||||
|
||||
if (action.type === 'PLUGIN_ADD') {
|
||||
const plugin = PluginService.instance().pluginById(action.plugin.id);
|
||||
this.protocolHandler_.allowReadAccessToDirectory(plugin.baseDir);
|
||||
this.protocolHandler_.allowReadAccessToDirectory(plugin.dataDir);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -204,7 +212,7 @@ class Application extends BaseApplication {
|
||||
public updateEditorFont() {
|
||||
const fontFamilies = [];
|
||||
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
|
||||
fontFamilies.push('Avenir, Arial, sans-serif');
|
||||
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
||||
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
@@ -353,16 +361,29 @@ class Application extends BaseApplication {
|
||||
Setting.setValue('wasClosedSuccessfully', false);
|
||||
}
|
||||
|
||||
private setupOcrService() {
|
||||
private async setupOcrService() {
|
||||
if (Setting.value('ocr.clearLanguageDataCache')) {
|
||||
Setting.setValue('ocr.clearLanguageDataCache', false);
|
||||
try {
|
||||
await OcrDriverTesseract.clearLanguageDataCache();
|
||||
} catch (error) {
|
||||
this.logger().warn('OCR: Failed to clear language data cache.', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (Setting.value('ocr.enabled')) {
|
||||
|
||||
if (!this.ocrService_) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const Tesseract = (window as any).Tesseract;
|
||||
|
||||
const driver = new OcrDriverTesseract(
|
||||
{ createWorker: Tesseract.createWorker },
|
||||
`${bridge().buildDir()}/tesseract.js/worker.min.js`,
|
||||
`${bridge().buildDir()}/tesseract.js-core`,
|
||||
{
|
||||
workerPath: `${bridge().buildDir()}/tesseract.js/worker.min.js`,
|
||||
corePath: `${bridge().buildDir()}/tesseract.js-core`,
|
||||
languageDataPath: Setting.value('ocr.languageDataPath') || null,
|
||||
},
|
||||
);
|
||||
|
||||
this.ocrService_ = new OcrService(driver);
|
||||
@@ -427,6 +448,20 @@ class Application extends BaseApplication {
|
||||
bridge().openDevTools();
|
||||
}
|
||||
|
||||
bridge().electronApp().initializeCustomProtocolHandler(
|
||||
Logger.create('handleCustomProtocols'),
|
||||
);
|
||||
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
|
||||
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('tempDir'));
|
||||
// For now, this doesn't seem necessary:
|
||||
// this.protocolHandler_.allowReadAccessTo(Setting.value('profileDir'));
|
||||
// If it is needed, note that they decrease the security of the protcol
|
||||
// handler, and, as such, it may make sense to also limit permissions of
|
||||
// allowed pages with a Content Security Policy.
|
||||
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
PluginManager.instance().setLogger(reg.logger());
|
||||
PluginManager.instance().register(pluginClasses);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions } from 'electron';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron';
|
||||
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { urlDecode } from '@joplin/lib/string-utils';
|
||||
@@ -485,6 +485,21 @@ export class Bridge {
|
||||
return nativeImage.createFromPath(path);
|
||||
}
|
||||
|
||||
public safeStorage = {
|
||||
isEncryptionAvailable() {
|
||||
return safeStorage.isEncryptionAvailable();
|
||||
},
|
||||
encryptString(data: string) {
|
||||
return safeStorage.encryptString(data).toString('base64');
|
||||
},
|
||||
decryptString(base64Data: string) {
|
||||
return safeStorage.decryptString(Buffer.from(base64Data, 'base64'));
|
||||
},
|
||||
|
||||
getSelectedStorageBackend() {
|
||||
return safeStorage.getSelectedStorageBackend();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let bridge_: Bridge = null;
|
||||
|
@@ -36,6 +36,9 @@ interface Props {
|
||||
isSquare?: boolean;
|
||||
iconOnly?: boolean;
|
||||
fontSize?: number;
|
||||
|
||||
'aria-controls'?: string;
|
||||
'aria-expanded'?: string;
|
||||
}
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
@@ -220,7 +223,14 @@ const Button = React.forwardRef((props: Props, ref: any) => {
|
||||
|
||||
function renderIcon() {
|
||||
if (!props.iconName) return null;
|
||||
return <StyledIcon aria-label={props.iconLabel} animation={props.iconAnimation} mr={iconOnly ? '0' : '6px'} color={props.color} className={props.iconName}/>;
|
||||
return <StyledIcon
|
||||
aria-label={props.iconLabel ?? ''}
|
||||
animation={props.iconAnimation}
|
||||
mr={iconOnly ? '0' : '6px'}
|
||||
color={props.color}
|
||||
className={props.iconName}
|
||||
role='img'
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderTitle() {
|
||||
@@ -234,7 +244,22 @@ const Button = React.forwardRef((props: Props, ref: any) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton ref={ref} fontSize={props.fontSize} isSquare={props.isSquare} size={props.size} style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
fontSize={props.fontSize}
|
||||
isSquare={props.isSquare}
|
||||
size={props.size}
|
||||
style={props.style}
|
||||
disabled={props.disabled}
|
||||
title={props.tooltip}
|
||||
className={props.className}
|
||||
iconOnly={iconOnly}
|
||||
onClick={onClick}
|
||||
|
||||
aria-disabled={props.disabled}
|
||||
aria-expanded={props['aria-expanded']}
|
||||
aria-controls={props['aria-controls']}
|
||||
>
|
||||
{renderIcon()}
|
||||
{renderTitle()}
|
||||
</StyledButton>
|
||||
|
@@ -1,16 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import Sidebar from './Sidebar';
|
||||
import ButtonBar from './ButtonBar';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../services/bridge';
|
||||
import Setting, { AppType, SettingItemSubType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||
import Setting, { AppType, SettingValueType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
import * as pathUtils from '@joplin/lib/path-utils';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
@@ -20,12 +18,8 @@ import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButto
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
import FontSearch from './FontSearch';
|
||||
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const settingKeyToControl: any = {
|
||||
'plugins.states': control_PluginsStates,
|
||||
};
|
||||
|
||||
interface Font {
|
||||
family: string;
|
||||
@@ -34,6 +28,7 @@ interface Font {
|
||||
declare global {
|
||||
interface Window {
|
||||
queryLocalFonts(): Promise<Font[]>;
|
||||
openChangelogLink: ()=> void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +62,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.onCancelClick = this.onCancelClick.bind(this);
|
||||
this.onSaveClick = this.onSaveClick.bind(this);
|
||||
this.onApplyClick = this.onApplyClick.bind(this);
|
||||
this.renderLabel = this.renderLabel.bind(this);
|
||||
this.renderDescription = this.renderDescription.bind(this);
|
||||
this.renderHeader = this.renderHeader.bind(this);
|
||||
this.handleSettingButton = this.handleSettingButton.bind(this);
|
||||
}
|
||||
|
||||
@@ -113,6 +105,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData);
|
||||
await Setting.saveAll();
|
||||
await restart();
|
||||
} else if (key === 'ocr.clearLanguageDataCacheButton') {
|
||||
if (!confirm(this.restartMessage())) return;
|
||||
Setting.setValue('ocr.clearLanguageDataCache', true);
|
||||
await restart();
|
||||
} else if (key === 'sync.openSyncWizard') {
|
||||
this.props.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
@@ -237,7 +233,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
<div style={statusStyle} aria-live='polite'>
|
||||
{messages[0]}
|
||||
{messages.length >= 1 ? <p>{messages[1]}</p> : null}
|
||||
</div>
|
||||
@@ -277,12 +273,14 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
let advancedSettingsButton = null;
|
||||
const advancedSettingsSectionStyle = { display: 'none' };
|
||||
const advancedSettingsGroupId = `advanced_settings_${key}`;
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
advancedSettingsButton = (
|
||||
<ToggleAdvancedSettingsButton
|
||||
onClick={() => shared.advancedSettingsButton_click(this)}
|
||||
advancedSettingsVisible={this.state.showAdvancedSettings}
|
||||
aria-controls={advancedSettingsGroupId}
|
||||
/>
|
||||
);
|
||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||
@@ -293,425 +291,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
{this.renderSectionDescription(section)}
|
||||
<div>{settingComps}</div>
|
||||
{advancedSettingsButton}
|
||||
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
|
||||
<div
|
||||
style={advancedSettingsSectionStyle}
|
||||
id={advancedSettingsGroupId}
|
||||
role='group'
|
||||
>{advancedSettingComps}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private labelStyle(themeId: number) {
|
||||
const theme = themeStyle(themeId);
|
||||
return { ...theme.textStyle, display: 'block',
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 1.083333,
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.mainPadding / 2 };
|
||||
}
|
||||
|
||||
private descriptionStyle(themeId: number) {
|
||||
const theme = themeStyle(themeId);
|
||||
return { ...theme.textStyle, color: theme.colorFaded,
|
||||
fontStyle: 'italic',
|
||||
maxWidth: '70em',
|
||||
marginTop: 5 };
|
||||
}
|
||||
|
||||
private renderLabel(themeId: number, label: string) {
|
||||
const labelStyle = this.labelStyle(themeId);
|
||||
return (
|
||||
<div style={labelStyle}>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private renderHeader(themeId: number, label: string, style: any = null) {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const labelStyle = { ...theme.textStyle, display: 'block',
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 1.25,
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.mainPadding,
|
||||
...style };
|
||||
|
||||
return (
|
||||
<div style={labelStyle}>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDescription(themeId: number, description: string) {
|
||||
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public settingToComponent(key: string, value: any) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const output: any = null;
|
||||
|
||||
const rowStyle = {
|
||||
marginBottom: theme.mainPadding * 1.5,
|
||||
};
|
||||
|
||||
const labelStyle = this.labelStyle(this.props.themeId);
|
||||
|
||||
const subLabel = { ...labelStyle, display: 'block',
|
||||
opacity: 0.7,
|
||||
marginBottom: labelStyle.marginBottom };
|
||||
|
||||
const checkboxLabelStyle = { ...labelStyle, marginLeft: 8,
|
||||
display: 'inline',
|
||||
backgroundColor: 'transparent' };
|
||||
|
||||
const controlStyle = {
|
||||
display: 'inline-block',
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
const textInputBaseStyle = { ...controlStyle, fontFamily: theme.fontFamily,
|
||||
border: '1px solid',
|
||||
padding: '4px 6px',
|
||||
boxSizing: 'border-box',
|
||||
borderColor: theme.borderColor4,
|
||||
borderRadius: 3,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4 };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const updateSettingValue = (key: string, value: any) => {
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.needRestart) {
|
||||
this.setState({ needRestart: true });
|
||||
}
|
||||
shared.updateSettingValue(this, key, value);
|
||||
};
|
||||
|
||||
private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent) => {
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
||||
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
|
||||
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
|
||||
|
||||
if (settingKeyToControl[key]) {
|
||||
const SettingComponent = settingKeyToControl[key];
|
||||
const label = md.label ? this.renderLabel(this.props.themeId, md.label()) : null;
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
{label}
|
||||
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
|
||||
<SettingComponent
|
||||
metadata={md}
|
||||
value={value}
|
||||
themeId={this.props.themeId}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
updateSettingValue(key, event.value);
|
||||
}}
|
||||
renderLabel={this.renderLabel}
|
||||
renderDescription={this.renderDescription}
|
||||
renderHeader={this.renderHeader}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (md.isEnum) {
|
||||
const items = [];
|
||||
const settingOptions = md.options();
|
||||
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
|
||||
valueKey: 'key',
|
||||
labelKey: 'label',
|
||||
});
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const e = array[i];
|
||||
items.push(
|
||||
<option value={e.key.toString()} key={e.key}>
|
||||
{settingOptions[e.key]}
|
||||
</option>,
|
||||
);
|
||||
}
|
||||
|
||||
const selectStyle = { ...controlStyle, paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
borderColor: theme.borderColor4,
|
||||
borderRadius: 3 };
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<select
|
||||
value={value}
|
||||
style={selectStyle}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}}
|
||||
>
|
||||
{items}
|
||||
</select>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
const onCheckboxClick = () => {
|
||||
updateSettingValue(key, !value);
|
||||
};
|
||||
|
||||
const checkboxSize = theme.fontSize * 1.1666666666666;
|
||||
|
||||
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
|
||||
// There's probably a better way to do this but can't figure it out.
|
||||
|
||||
return (
|
||||
<div key={key + (`${value}`).toString()} style={rowStyle}>
|
||||
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
id={`setting_checkbox_${key}`}
|
||||
type="checkbox"
|
||||
checked={!!value}
|
||||
onChange={() => {
|
||||
onCheckboxClick();
|
||||
}}
|
||||
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
|
||||
/>
|
||||
<label
|
||||
onClick={() => {
|
||||
onCheckboxClick();
|
||||
}}
|
||||
style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
|
||||
htmlFor={`setting_checkbox_${key}`}
|
||||
>
|
||||
{md.label()}
|
||||
</label>
|
||||
</div>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
|
||||
minWidth: '20em' };
|
||||
const inputType = md.secure === true ? 'password' : 'text';
|
||||
|
||||
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
|
||||
inputStyle.marginBottom = subLabel.marginBottom;
|
||||
|
||||
const splitCmd = (cmdString: string) => {
|
||||
// Normally not necessary but certain plugins found a way to
|
||||
// set the set the value to "undefined", leading to a crash.
|
||||
// This is now fixed at the model level but to be sure we
|
||||
// check here too, to handle any already existing data.
|
||||
// https://github.com/laurent22/joplin/issues/7621
|
||||
if (!cmdString) cmdString = '';
|
||||
const path = pathUtils.extractExecutablePath(cmdString);
|
||||
const args = cmdString.substr(path.length + 1);
|
||||
return [pathUtils.unquotePath(path), args];
|
||||
};
|
||||
|
||||
const joinCmd = (cmdArray: string[]) => {
|
||||
if (!cmdArray[0] && !cmdArray[1]) return '';
|
||||
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
||||
if (!cmdString) cmdString = '""';
|
||||
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
|
||||
return cmdString;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onPathChange = (event: any) => {
|
||||
if (md.subType === 'file_path_and_args') {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[0] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
} else {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onArgsChange = (event: any) => {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[1] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
};
|
||||
|
||||
const browseButtonClick = async () => {
|
||||
if (md.subType === 'directory_path') {
|
||||
const paths = await bridge().showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (!paths || !paths.length) return;
|
||||
updateSettingValue(key, paths[0]);
|
||||
} else {
|
||||
const paths = await bridge().showOpenDialog();
|
||||
if (!paths || !paths.length) return;
|
||||
|
||||
if (md.subType === 'file_path') {
|
||||
updateSettingValue(key, paths[0]);
|
||||
} else {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[0] = paths[0];
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
const path = md.subType === 'file_path_and_args' ? cmd[0] : this.state.settings[key];
|
||||
|
||||
const argComp = md.subType !== 'file_path_and_args' ? null : (
|
||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||
<div style={subLabel}>{_('Arguments:')}</div>
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onArgsChange(event);
|
||||
}}
|
||||
value={cmd[1]}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||
<div style={subLabel}>{_('Path:')}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
|
||||
<input
|
||||
type={inputType}
|
||||
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onPathChange(event);
|
||||
}}
|
||||
value={path}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<Button
|
||||
level={ButtonLevel.Secondary}
|
||||
title={_('Browse...')}
|
||||
onClick={browseButtonClick}
|
||||
size={ButtonSize.Small}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{argComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onTextChange = (event: any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
{
|
||||
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
|
||||
<FontSearch
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
availableFonts={this.state.fonts}
|
||||
onChange={fontFamily => updateSettingValue(key, fontFamily)}
|
||||
subtype={md.subType}
|
||||
/> :
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onTextChange(event);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
}
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onNumChange = (event: any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
const label = [md.label()];
|
||||
if (md.unitLabel) label.push(`(${md.unitLabel()})`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const inputStyle: any = { ...textInputBaseStyle };
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{label.join(' ')}</label>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onNumChange(event);
|
||||
}}
|
||||
min={md.minimum}
|
||||
max={md.maximum}
|
||||
step={md.step}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
const labelComp = md.hideLabel ? null : (
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
{labelComp}
|
||||
<Button level={ButtonLevel.Secondary} title={md.label()} onClick={md.onClick ? md.onClick : () => this.handleSettingButton(key)}/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn(`Type not implemented: ${key}`);
|
||||
if (md.needRestart) {
|
||||
this.setState({ needRestart: true });
|
||||
}
|
||||
shared.updateSettingValue(this, key, value);
|
||||
};
|
||||
|
||||
return output;
|
||||
public settingToComponent<T extends string>(key: T, value: SettingValueType<T>) {
|
||||
return (
|
||||
<SettingComponent
|
||||
themeId={this.props.themeId}
|
||||
key={key}
|
||||
settingKey={key}
|
||||
value={value}
|
||||
fonts={this.state.fonts}
|
||||
onUpdateSettingValue={this.onUpdateSettingValue}
|
||||
onSettingButtonClick={this.handleSettingButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private restartMessage() {
|
||||
@@ -768,7 +376,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
const settings = this.state.settings;
|
||||
|
||||
const containerStyle = {
|
||||
const containerStyle: React.CSSProperties = {
|
||||
overflow: 'auto',
|
||||
padding: theme.configScreenPadding,
|
||||
paddingTop: 0,
|
||||
@@ -800,6 +408,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
const rightStyle = { ...style, flex: 1 };
|
||||
delete style.width;
|
||||
|
||||
const tabComponents: React.ReactNode[] = [];
|
||||
for (const section of sections) {
|
||||
const sectionId = `setting-section-${section.name}`;
|
||||
let content = null;
|
||||
const visible = section.name === this.state.selectedSectionName;
|
||||
if (visible) {
|
||||
content = (
|
||||
<>
|
||||
{screenComp}
|
||||
<div style={containerStyle}>{settingComps}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
tabComponents.push(
|
||||
<div
|
||||
key={sectionId}
|
||||
id={sectionId}
|
||||
className={`setting-tab-panel ${!visible ? '-hidden' : ''}`}
|
||||
hidden={!visible}
|
||||
aria-labelledby={`setting-tab-${section.name}`}
|
||||
tabIndex={0}
|
||||
role='tabpanel'
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
|
||||
<Sidebar
|
||||
@@ -808,9 +445,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
sections={sections}
|
||||
/>
|
||||
<div style={rightStyle}>
|
||||
{screenComp}
|
||||
{needRestartComp}
|
||||
<div style={containerStyle}>{settingComps}</div>
|
||||
{tabComponents}
|
||||
<ButtonBar
|
||||
hasChanges={hasChanges}
|
||||
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
|
||||
|
@@ -1,18 +1,22 @@
|
||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { AppType, MetadataBySection, SettingMetadataSection, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import * as React from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
type StyleProps = any;
|
||||
|
||||
interface SectionChangeEvent {
|
||||
section: SettingMetadataSection;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selection: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onSelectionChange: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
sections: any[];
|
||||
onSelectionChange: (event: SectionChangeEvent)=> void;
|
||||
sections: MetadataBySection;
|
||||
}
|
||||
|
||||
export const StyledRoot = styled.div`
|
||||
@@ -73,24 +77,63 @@ export const StyledListItemIcon = styled.i`
|
||||
`;
|
||||
|
||||
export default function Sidebar(props: Props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
const buttons: any[] = [];
|
||||
const buttonRefs = useRef<HTMLElement[]>([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
function renderButton(section: any) {
|
||||
// Making a tabbed region accessible involves supporting keyboard interaction.
|
||||
// See https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ for details
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback((event) => {
|
||||
const selectedIndex = props.sections.findIndex(section => section.name === props.selection);
|
||||
let newIndex = selectedIndex;
|
||||
|
||||
if (event.code === 'ArrowUp') {
|
||||
newIndex --;
|
||||
} else if (event.code === 'ArrowDown') {
|
||||
newIndex ++;
|
||||
} else if (event.code === 'Home') {
|
||||
newIndex = 0;
|
||||
} else if (event.code === 'End') {
|
||||
newIndex = props.sections.length - 1;
|
||||
}
|
||||
|
||||
if (newIndex < 0) newIndex += props.sections.length;
|
||||
newIndex %= props.sections.length;
|
||||
|
||||
if (newIndex !== selectedIndex) {
|
||||
event.preventDefault();
|
||||
props.onSelectionChange({ section: props.sections[newIndex] });
|
||||
|
||||
const targetButton = buttonRefs.current[newIndex];
|
||||
if (targetButton) {
|
||||
focus('Sidebar', targetButton);
|
||||
}
|
||||
}
|
||||
}, [props.sections, props.selection, props.onSelectionChange]);
|
||||
|
||||
const buttons: React.ReactNode[] = [];
|
||||
|
||||
function renderButton(section: SettingMetadataSection, index: number) {
|
||||
const selected = props.selection === section.name;
|
||||
return (
|
||||
<StyledListItem
|
||||
key={section.name}
|
||||
href='#'
|
||||
role='tab'
|
||||
ref={(item: HTMLElement) => { buttonRefs.current[index] = item; }}
|
||||
|
||||
id={`setting-tab-${section.name}`}
|
||||
aria-controls={`setting-section-${section.name}`}
|
||||
aria-selected={selected}
|
||||
tabIndex={selected ? 0 : -1}
|
||||
|
||||
isSubSection={Setting.isSubSection(section.name)}
|
||||
selected={selected}
|
||||
onClick={() => { props.onSelectionChange({ section: section }); }}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<StyledListItemIcon
|
||||
aria-label=''
|
||||
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
||||
role='img'
|
||||
/>
|
||||
<StyledListItemLabel>
|
||||
{Setting.sectionNameToLabel(section.name)}
|
||||
@@ -109,13 +152,15 @@ export default function Sidebar(props: Props) {
|
||||
|
||||
let pluginDividerAdded = false;
|
||||
|
||||
let index = 0;
|
||||
for (const section of props.sections) {
|
||||
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
|
||||
buttons.push(renderDivider('divider-plugins'));
|
||||
pluginDividerAdded = true;
|
||||
}
|
||||
|
||||
buttons.push(renderButton(section));
|
||||
buttons.push(renderButton(section, index));
|
||||
index ++;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -9,6 +9,7 @@ interface Props {
|
||||
style: CSSProperties;
|
||||
value: string;
|
||||
availableFonts: string[];
|
||||
inputId: string;
|
||||
onChange: (font: string)=> void;
|
||||
subtype: string;
|
||||
}
|
||||
@@ -108,6 +109,7 @@ const FontSearch = (props: Props) => {
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
spellCheck={false}
|
||||
id={props.inputId}
|
||||
ref={fontInputRef}
|
||||
/>
|
||||
<div
|
@@ -0,0 +1,381 @@
|
||||
import Setting, { AppType, SettingItemSubType } from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useId } from 'react';
|
||||
import control_PluginsStates from './plugins/PluginsStates';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../../Button/Button';
|
||||
import FontSearch from './FontSearch';
|
||||
import * as pathUtils from '@joplin/lib/path-utils';
|
||||
import SettingLabel from './SettingLabel';
|
||||
import SettingDescription from './SettingDescription';
|
||||
|
||||
const settingKeyToControl: Record<string, typeof control_PluginsStates> = {
|
||||
'plugins.states': control_PluginsStates,
|
||||
};
|
||||
|
||||
export interface UpdateSettingValueEvent {
|
||||
key: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
settingKey: string;
|
||||
value: unknown;
|
||||
fonts: string[];
|
||||
onUpdateSettingValue: (event: UpdateSettingValueEvent)=> void;
|
||||
onSettingButtonClick: (key: string)=> void;
|
||||
}
|
||||
|
||||
const SettingComponent: React.FC<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const output: React.ReactNode = null;
|
||||
|
||||
const updateSettingValue = useCallback((key: string, value: unknown) => {
|
||||
props.onUpdateSettingValue({ key, value });
|
||||
}, [props.onUpdateSettingValue]);
|
||||
|
||||
const rowStyle = {
|
||||
marginBottom: theme.mainPadding * 1.5,
|
||||
};
|
||||
|
||||
const controlStyle = {
|
||||
display: 'inline-block',
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
const textInputBaseStyle: React.CSSProperties = {
|
||||
...controlStyle,
|
||||
fontFamily: theme.fontFamily,
|
||||
border: '1px solid',
|
||||
padding: '4px 6px',
|
||||
boxSizing: 'border-box',
|
||||
borderColor: theme.borderColor4,
|
||||
borderRadius: 3,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
};
|
||||
|
||||
const key = props.settingKey;
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
||||
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
|
||||
const inputId = useId();
|
||||
const descriptionId = useId();
|
||||
const descriptionComp = <SettingDescription id={descriptionId} text={descriptionText}/>;
|
||||
|
||||
if (key in settingKeyToControl) {
|
||||
const CustomSettingComponent = settingKeyToControl[key];
|
||||
const label = md.label ? <SettingLabel text={md.label()} htmlFor={null} /> : null;
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
{label}
|
||||
<SettingDescription id={descriptionId} text={md.description ? md.description(AppType.Desktop) : null}/>
|
||||
<CustomSettingComponent
|
||||
value={props.value}
|
||||
themeId={props.themeId}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
updateSettingValue(key, event.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (md.isEnum) {
|
||||
const value = props.value as string;
|
||||
|
||||
const items = [];
|
||||
const settingOptions = md.options();
|
||||
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
|
||||
valueKey: 'key',
|
||||
labelKey: 'label',
|
||||
});
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const e = array[i];
|
||||
items.push(
|
||||
<option value={e.key.toString()} key={e.key}>
|
||||
{settingOptions[e.key]}
|
||||
</option>,
|
||||
);
|
||||
}
|
||||
|
||||
const selectStyle = { ...controlStyle, paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
borderColor: theme.borderColor4,
|
||||
borderRadius: 3 };
|
||||
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
<SettingLabel htmlFor={inputId} text={md.label()}/>
|
||||
<select
|
||||
value={value}
|
||||
style={selectStyle}
|
||||
onChange={(event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}}
|
||||
id={inputId}
|
||||
aria-describedby={descriptionId}
|
||||
>
|
||||
{items}
|
||||
</select>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
const value = props.value as boolean;
|
||||
|
||||
const checkboxSize = theme.fontSize * 1.1666666666666;
|
||||
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
id={inputId}
|
||||
type="checkbox"
|
||||
checked={!!value}
|
||||
onChange={event => updateSettingValue(key, event.target.checked)}
|
||||
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
|
||||
|
||||
// Prefer aria-details to aria-describedby for checkbox inputs --
|
||||
// on MacOS, VoiceOver reads "checked"/"unchecked" only after reading the
|
||||
// potentially-lengthy description. For other input types, the input value
|
||||
// is read first.
|
||||
aria-details={descriptionId}
|
||||
/>
|
||||
<label
|
||||
className='setting-label -for-checkbox'
|
||||
htmlFor={inputId}
|
||||
>
|
||||
{md.label()}
|
||||
</label>
|
||||
</div>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
const value = props.value as string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
|
||||
minWidth: '20em' };
|
||||
const inputType = md.secure === true ? 'password' : 'text';
|
||||
|
||||
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
|
||||
inputStyle.marginBottom = theme.mainPadding / 2;
|
||||
|
||||
const splitCmd = (cmdString: string) => {
|
||||
// Normally not necessary but certain plugins found a way to
|
||||
// set the set the value to "undefined", leading to a crash.
|
||||
// This is now fixed at the model level but to be sure we
|
||||
// check here too, to handle any already existing data.
|
||||
// https://github.com/laurent22/joplin/issues/7621
|
||||
if (!cmdString) cmdString = '';
|
||||
const path = pathUtils.extractExecutablePath(cmdString);
|
||||
const args = cmdString.substr(path.length + 1);
|
||||
return [pathUtils.unquotePath(path), args];
|
||||
};
|
||||
|
||||
const joinCmd = (cmdArray: string[]) => {
|
||||
if (!cmdArray[0] && !cmdArray[1]) return '';
|
||||
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
||||
if (!cmdString) cmdString = '""';
|
||||
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
|
||||
return cmdString;
|
||||
};
|
||||
|
||||
const onPathChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||
if (md.subType === 'file_path_and_args') {
|
||||
const cmd = splitCmd(value);
|
||||
cmd[0] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
} else {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onArgsChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||
const cmd = splitCmd(value);
|
||||
cmd[1] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
};
|
||||
|
||||
const browseButtonClick = async () => {
|
||||
if (md.subType === 'directory_path') {
|
||||
const paths = await bridge().showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (!paths || !paths.length) return;
|
||||
updateSettingValue(key, paths[0]);
|
||||
} else {
|
||||
const paths = await bridge().showOpenDialog();
|
||||
if (!paths || !paths.length) return;
|
||||
|
||||
if (md.subType === 'file_path') {
|
||||
updateSettingValue(key, paths[0]);
|
||||
} else {
|
||||
const cmd = splitCmd(value);
|
||||
cmd[0] = paths[0];
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cmd = splitCmd(value);
|
||||
const path = md.subType === 'file_path_and_args' ? cmd[0] : value;
|
||||
|
||||
const argInputId = `setting_path_arg_${key}`;
|
||||
const argComp = md.subType !== 'file_path_and_args' ? null : (
|
||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||
<label
|
||||
className='setting-label -sub-label'
|
||||
htmlFor={argInputId}
|
||||
>{_('Arguments:')}</label>
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
onChange={onArgsChange}
|
||||
value={cmd[1]}
|
||||
spellCheck={false}
|
||||
id={argInputId}
|
||||
aria-describedby={descriptionId}
|
||||
/>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const pathDescriptionId = `setting_path_label_${key}`;
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
<SettingLabel text={md.label()} htmlFor={inputId}/>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||
<div
|
||||
className='setting-label -sub-label'
|
||||
id={pathDescriptionId}
|
||||
>{_('Path:')}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
|
||||
<input
|
||||
type={inputType}
|
||||
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
|
||||
onChange={onPathChange}
|
||||
value={path}
|
||||
spellCheck={false}
|
||||
id={inputId}
|
||||
aria-describedby={pathDescriptionId}
|
||||
aria-details={descriptionId}
|
||||
/>
|
||||
<Button
|
||||
level={ButtonLevel.Secondary}
|
||||
title={_('Browse...')}
|
||||
onClick={browseButtonClick}
|
||||
size={ButtonSize.Small}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{argComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const onTextChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
<SettingLabel text={md.label()} htmlFor={inputId}/>
|
||||
{
|
||||
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
|
||||
<FontSearch
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={props.value as string}
|
||||
availableFonts={props.fonts}
|
||||
onChange={fontFamily => updateSettingValue(key, fontFamily)}
|
||||
subtype={md.subType}
|
||||
inputId={inputId}
|
||||
/> :
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={props.value as string|number}
|
||||
onChange={onTextChange}
|
||||
spellCheck={false}
|
||||
id={inputId}
|
||||
aria-describedby={descriptionId}
|
||||
/>
|
||||
}
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const value = props.value as number;
|
||||
|
||||
const onNumChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
const label = [md.label()];
|
||||
if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`);
|
||||
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
<SettingLabel htmlFor={inputId} text={label.join(' ')}/>
|
||||
<input
|
||||
type="number"
|
||||
style={textInputBaseStyle}
|
||||
value={value}
|
||||
onChange={onNumChange}
|
||||
min={md.minimum}
|
||||
max={md.maximum}
|
||||
step={md.step}
|
||||
spellCheck={false}
|
||||
id={inputId}
|
||||
aria-describedby={descriptionId}
|
||||
/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
const labelComp = md.hideLabel ? null : (
|
||||
<SettingLabel text={md.label()} htmlFor={null} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={rowStyle}>
|
||||
{labelComp}
|
||||
<Button
|
||||
level={ButtonLevel.Secondary}
|
||||
title={md.label()}
|
||||
onClick={md.onClick ? md.onClick : () => props.onSettingButtonClick(key)}
|
||||
/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn(`Type not implemented: ${key}`);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default SettingComponent;
|
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const SettingDescription: React.FC<Props> = props => {
|
||||
return props.text ? <div className='setting-description' id={props.id}>{props.text}</div> : null;
|
||||
};
|
||||
|
||||
export default SettingDescription;
|
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const SettingHeader: React.FC<Props> = props => {
|
||||
return (
|
||||
<div className='setting-header'>
|
||||
<label>{props.text}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingHeader;
|
@@ -0,0 +1,16 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
htmlFor: string|null;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const SettingLabel: React.FC<Props> = props => {
|
||||
return (
|
||||
<div className='setting-label'>
|
||||
<label htmlFor={props.htmlFor}>{props.text}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingLabel;
|
@@ -6,6 +6,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
interface Props {
|
||||
onClick: ()=> void;
|
||||
advancedSettingsVisible: boolean;
|
||||
'aria-controls': string;
|
||||
}
|
||||
|
||||
const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
||||
@@ -16,6 +17,10 @@ const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
||||
level={ButtonLevel.Secondary}
|
||||
onClick={props.onClick}
|
||||
iconName={iconName}
|
||||
|
||||
aria-controls={props['aria-controls']}
|
||||
aria-expanded={props.advancedSettingsVisible}
|
||||
|
||||
title={_('Show Advanced Settings')}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useId, useMemo } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
||||
@@ -173,6 +173,7 @@ export default function(props: Props) {
|
||||
themeId={props.themeId}
|
||||
value={item.enabled}
|
||||
onToggle={() => props.onToggle({ item })}
|
||||
aria-label={_('Enabled')}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -256,10 +257,17 @@ export default function(props: Props) {
|
||||
return <RecommendedBadge href="#" title={_('The Joplin team has vetted this plugin and it meets our standards for security and performance.')} onClick={onRecommendedClick}><i className="fas fa-crown"></i></RecommendedBadge>;
|
||||
}
|
||||
|
||||
const nameLabelId = useId();
|
||||
|
||||
return (
|
||||
<CellRoot isCompatible={props.isCompatible}>
|
||||
<CellRoot isCompatible={props.isCompatible} role='group' aria-labelledby={nameLabelId}>
|
||||
<CellTop>
|
||||
<StyledNameAndVersion mb={'5px'}><StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }}>{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}</StyledName><StyledVersion>v{item.manifest.version}</StyledVersion></StyledNameAndVersion>
|
||||
<StyledNameAndVersion mb={'5px'}>
|
||||
<StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }} id={nameLabelId}>
|
||||
{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}
|
||||
</StyledName>
|
||||
<StyledVersion>v{item.manifest.version}</StyledVersion>
|
||||
</StyledNameAndVersion>
|
||||
{renderToggleButton()}
|
||||
{renderRecommendedBadge()}
|
||||
</CellTop>
|
||||
|
@@ -17,6 +17,8 @@ import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/use
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import StyledMessage from '../../../style/StyledMessage';
|
||||
import StyledLink from '../../../style/StyledLink';
|
||||
import SettingHeader from '../SettingHeader';
|
||||
import SettingDescription from '../SettingDescription';
|
||||
const { space } = require('styled-system');
|
||||
|
||||
const logger = Logger.create('PluginState');
|
||||
@@ -51,12 +53,6 @@ interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onChange: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderLabel: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderDescription: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderHeader: Function;
|
||||
}
|
||||
|
||||
let repoApi_: RepositoryApi = null;
|
||||
@@ -281,7 +277,7 @@ export default function(props: Props) {
|
||||
if (!pluginItems.length || allDeleted) {
|
||||
return (
|
||||
<UserPluginsRoot mb={'10px'}>
|
||||
{props.renderDescription(props.themeId, _('You do not have any installed plugin.'))}
|
||||
<SettingDescription text={_('You do not have any installed plugin.')}/>
|
||||
</UserPluginsRoot>
|
||||
);
|
||||
} else {
|
||||
@@ -311,7 +307,6 @@ export default function(props: Props) {
|
||||
pluginSettings={pluginSettings}
|
||||
onSearchQueryChange={onSearchQueryChange}
|
||||
onPluginSettingsChange={onSearchPluginSettingsChange}
|
||||
renderDescription={props.renderDescription}
|
||||
repoApi={repoApi}
|
||||
/>
|
||||
</div>
|
||||
@@ -333,7 +328,7 @@ export default function(props: Props) {
|
||||
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
|
||||
<ToolsButton size={ButtonSize.Small} tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
{props.renderHeader(props.themeId, _('Manage your plugins'))}
|
||||
<SettingHeader text={_('Manage your plugins')}/>
|
||||
</div>
|
||||
</div>
|
||||
{renderUserPlugins(pluginItems)}
|
||||
|
@@ -10,6 +10,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import SettingDescription from '../SettingDescription';
|
||||
|
||||
const Root = styled.div`
|
||||
`;
|
||||
@@ -26,8 +27,6 @@ interface Props {
|
||||
pluginSettings: PluginSettings;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onPluginSettingsChange(event: any): void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderDescription: Function;
|
||||
maxWidth: number;
|
||||
repoApi(): RepositoryApi;
|
||||
disabled: boolean;
|
||||
@@ -81,7 +80,7 @@ export default function(props: Props) {
|
||||
function renderResults(query: string, manifests: PluginManifest[]) {
|
||||
if (query && !manifests.length) {
|
||||
if (searchResultCount === null) return ''; // Search in progress
|
||||
return props.renderDescription(props.themeId, _('No results'));
|
||||
return <SettingDescription text={_('No results')}/>;
|
||||
} else {
|
||||
const output = [];
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@use "./styles/index.scss";
|
||||
|
||||
.config-screen-content-wrapper {
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
|
5
packages/app-desktop/gui/ConfigScreen/styles/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
@use "./setting-description.scss";
|
||||
@use "./setting-label.scss";
|
||||
@use "./setting-header.scss";
|
||||
@use "./setting-tab-panel.scss";
|
@@ -0,0 +1,9 @@
|
||||
|
||||
.setting-description {
|
||||
color: var(--joplin-color-faded);
|
||||
font-size: var(--joplin-font-size);
|
||||
line-height: var(--joplin-line-height);
|
||||
font-style: italic;
|
||||
max-width: 70em;
|
||||
margin-top: 5px;
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
|
||||
.setting-header {
|
||||
display: block;
|
||||
color: var(--joplin-color);
|
||||
font-size: calc(var(--joplin-font-size) * 1.25);
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--joplin-main-padding);
|
||||
line-height: var(--joplin-line-height);
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
color: var(--joplin-color);
|
||||
font-size: calc(var(--joplin-font-size) * 1.083333);
|
||||
font-weight: 500;
|
||||
margin-bottom: calc(var(--joplin-main-padding) / 2);
|
||||
line-height: var(--joplin-line-height);
|
||||
|
||||
&.-sub-label {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.-for-checkbox {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 0;
|
||||
display: inline;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
|
||||
.setting-tab-panel {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
|
||||
&.-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
// Use a border rather than an outline -- an outline would be shown outside of the screen
|
||||
// and thus invisible.
|
||||
border: 1px solid var(--joplin-focus-outline-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
@@ -1,49 +1,128 @@
|
||||
import styled from 'styled-components';
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { blur, focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
const DialogModalLayer = styled.div`
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const DialogRoot = styled.div`
|
||||
background-color: ${props => props.theme.backgroundColor};
|
||||
padding: 16px;
|
||||
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
|
||||
margin: 20px;
|
||||
min-height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
`;
|
||||
type OnCancelListener = ()=> void;
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
renderContent: Function;
|
||||
className?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose?: Function;
|
||||
onCancel?: OnCancelListener;
|
||||
contentStyle?: React.CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Dialog(props: Props) {
|
||||
return (
|
||||
<DialogModalLayer className={props.className}>
|
||||
<DialogRoot>
|
||||
{props.renderContent()}
|
||||
</DialogRoot>
|
||||
</DialogModalLayer>
|
||||
const Dialog: React.FC<Props> = props => {
|
||||
// For correct focus handling, the dialog element needs to be managed separately from React. In particular,
|
||||
// just after creating the dialog, we need to call .showModal() and just **before** closing the dialog, we
|
||||
// need to call .close(). This second requirement is particularly difficult, as this needs to happen even
|
||||
// if the dialog is closed by removing its parent from the React DOM.
|
||||
//
|
||||
// Because useEffect cleanup can happen after an element is removed from the HTML DOM, the dialog is managed
|
||||
// using native HTML APIs. This allows us to call .close() while the dialog is still attached to the DOM, which
|
||||
// allows the browser to restore the focus from before the dialog was opened.
|
||||
const dialogElement = useDialogElement(props.onCancel);
|
||||
useDialogClassNames(dialogElement, props.className);
|
||||
|
||||
const [contentRendered, setContentRendered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogElement || !contentRendered) return;
|
||||
|
||||
if (!dialogElement.open) {
|
||||
dialogElement.showModal();
|
||||
}
|
||||
}, [dialogElement, contentRendered]);
|
||||
|
||||
if (dialogElement && !contentRendered) {
|
||||
setContentRendered(true);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className='content' style={props.contentStyle}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <>
|
||||
{dialogElement && createPortal(content, dialogElement)}
|
||||
</>;
|
||||
};
|
||||
|
||||
const useDialogElement = (onCancel: undefined|OnCancelListener) => {
|
||||
const [dialogElement, setDialogElement] = useState<HTMLDialogElement|null>(null);
|
||||
|
||||
const onCancelRef = useRef(onCancel);
|
||||
onCancelRef.current = onCancel;
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = document.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;
|
||||
if (!canCancel) {
|
||||
// Prevents [Escape] from closing the dialog. In many places, this is handled
|
||||
// by external logic.
|
||||
// See https://stackoverflow.com/a/61021326
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const removedReturnValue = 'removed-from-dom';
|
||||
dialog.addEventListener('close', () => {
|
||||
const closedByCancel = dialog.returnValue !== removedReturnValue;
|
||||
if (closedByCancel) {
|
||||
onCancelRef.current?.();
|
||||
}
|
||||
|
||||
// Work around what seems to be an Electron bug -- if an input or contenteditable region is refocused after
|
||||
// dismissing a dialog, it won't be editable.
|
||||
// Note: While this addresses the issue in the note title input, it does not address the issue in the Rich Text Editor.
|
||||
if (document.activeElement?.tagName === 'INPUT') {
|
||||
const element = document.activeElement as HTMLElement;
|
||||
blur('Dialog', element);
|
||||
focus('Dialog', element);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
setDialogElement(dialog);
|
||||
|
||||
return () => {
|
||||
if (dialog.open) {
|
||||
// .close: Instructs the browser to restore keyboard focus to whatever was focused
|
||||
// before the dialog.
|
||||
dialog.close(removedReturnValue);
|
||||
}
|
||||
dialog.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return dialogElement;
|
||||
};
|
||||
|
||||
const useDialogClassNames = (dialogElement: HTMLElement|null, classNames: undefined|string) => {
|
||||
useEffect(() => {
|
||||
if (!dialogElement || !classNames) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// The React className prop can include multiple space-separated classes
|
||||
const newClassNames = classNames
|
||||
.split(/\s+/)
|
||||
.filter(name => name.length && !dialogElement.classList.contains(name));
|
||||
dialogElement.classList.add(...newClassNames);
|
||||
|
||||
return () => {
|
||||
dialogElement.classList.remove(...newClassNames);
|
||||
};
|
||||
}, [dialogElement, classNames]);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { isInsideContainer } from '@joplin/lib/dom';
|
||||
|
||||
@@ -40,8 +41,7 @@ export default (props: Props) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onKeyDown = useCallback((event: any) => {
|
||||
const onKeyDown = useCallback((event: KeyboardEvent|React.KeyboardEvent) => {
|
||||
// Early exit if it's neither ENTER nor ESCAPE, because isInSubModal
|
||||
// function can be costly.
|
||||
if (event.keyCode !== 13 && event.keyCode !== 27) return;
|
||||
@@ -49,8 +49,12 @@ export default (props: Props) => {
|
||||
if (!isTopDialog() || isInSubModal(event.target)) return;
|
||||
|
||||
if (event.keyCode === 13) {
|
||||
if (event.target.nodeName !== 'TEXTAREA') {
|
||||
props.onOkButtonClick();
|
||||
if ('nodeName' in event.target && event.target.nodeName === 'INPUT') {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
if (target.type !== 'button' && target.type !== 'checkbox') {
|
||||
props.onOkButtonClick();
|
||||
}
|
||||
}
|
||||
} else if (event.keyCode === 27) {
|
||||
props.onCancelButtonClick();
|
||||
|
@@ -179,6 +179,6 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} className="master-password-dialog" renderContent={renderDialogWrapper}/>
|
||||
<Dialog onCancel={onClose} className="master-password-dialog">{renderDialogWrapper()}</Dialog>
|
||||
);
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ interface Props {
|
||||
export const IconSelector = (props: Props) => {
|
||||
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
|
||||
const [picker, setPicker] = useState<EmojiButton>();
|
||||
const buttonRef = useRef(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const loadScripts = async () => {
|
||||
@@ -61,6 +61,7 @@ export const IconSelector = (props: Props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const p: EmojiButton = new (window as any).EmojiButton({
|
||||
zIndex: 10000,
|
||||
rootElement: buttonRef.current?.parentElement,
|
||||
});
|
||||
|
||||
const onEmoji = (selection: FolderIcon) => {
|
||||
@@ -73,6 +74,7 @@ export const IconSelector = (props: Props) => {
|
||||
|
||||
return () => {
|
||||
p.off('emoji', onEmoji);
|
||||
p.destroyPicker();
|
||||
};
|
||||
}, [emojiButtonClassReady, props.onChange]);
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useId, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -350,7 +350,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<h2>{_('Re-encryption')}</h2>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
@@ -358,7 +358,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
</span>
|
||||
|
||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -368,6 +368,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}, [showAdvanced]);
|
||||
|
||||
const advancedSettingsId = useId();
|
||||
const renderAdvancedSection = () => {
|
||||
const reEncryptSection = renderReencryptData();
|
||||
|
||||
@@ -378,8 +379,12 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
<div>
|
||||
<ToggleAdvancedSettingsButton
|
||||
onClick={toggleAdvanced}
|
||||
advancedSettingsVisible={showAdvanced}/>
|
||||
{ showAdvanced ? reEncryptSection : null }
|
||||
advancedSettingsVisible={showAdvanced}
|
||||
aria-controls={advancedSettingsId}
|
||||
/>
|
||||
<div id={advancedSettingsId}>
|
||||
{ showAdvanced ? reEncryptSection : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
tip: string;
|
||||
@@ -10,6 +11,9 @@ interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
|
||||
'aria-controls'?: string;
|
||||
'aria-expanded'?: string;
|
||||
}
|
||||
|
||||
class HelpButtonComponent extends React.Component<Props> {
|
||||
@@ -29,11 +33,21 @@ class HelpButtonComponent extends React.Component<Props> {
|
||||
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const extraProps: any = {};
|
||||
if (this.props.tip) extraProps['data-tip'] = this.props.tip;
|
||||
if (this.props.tip) {
|
||||
extraProps['data-tip'] = this.props.tip;
|
||||
extraProps['aria-description'] = this.props.tip;
|
||||
}
|
||||
return (
|
||||
<a href="#" style={style} onClick={this.onClick} {...extraProps}>
|
||||
<i style={helpIconStyle} className={'fa fa-question-circle'}></i>
|
||||
</a>
|
||||
<button
|
||||
style={style}
|
||||
onClick={this.onClick}
|
||||
className='flat-button'
|
||||
aria-controls={this.props['aria-controls']}
|
||||
aria-expanded={this.props['aria-expanded']}
|
||||
{...extraProps}
|
||||
>
|
||||
<i style={helpIconStyle} className={'fa fa-question-circle'} role='img' aria-label={_('Help')}></i>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,10 @@ interface Props<ItemType> {
|
||||
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
|
||||
className?: string;
|
||||
onItemDrop?: DragEventHandler<HTMLElement>;
|
||||
|
||||
id?: string;
|
||||
role?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -164,7 +168,20 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
|
||||
if (this.props.className) classes.push(this.props.className);
|
||||
|
||||
return (
|
||||
<div ref={this.listRef} className={classes.join(' ')} style={style} onScroll={this.onScroll} onKeyDown={this.onKeyDown} onDrop={this.onDrop}>
|
||||
<div
|
||||
ref={this.listRef}
|
||||
className={classes.join(' ')}
|
||||
style={style}
|
||||
|
||||
id={this.props.id}
|
||||
role={this.props.role}
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-setsize={items.length}
|
||||
|
||||
onScroll={this.onScroll}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
{itemComps}
|
||||
</div>
|
||||
);
|
||||
|
@@ -48,6 +48,7 @@ import NotePropertiesDialog from '../NotePropertiesDialog';
|
||||
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import validateColumns from '../NoteListHeader/utils/validateColumns';
|
||||
import TrashNotification from '../TrashNotification/TrashNotification';
|
||||
import UpdateNotification from '../UpdateNotification/UpdateNotification';
|
||||
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
@@ -85,7 +86,7 @@ interface Props {
|
||||
startupPluginsLoaded: boolean;
|
||||
shareInvitations: ShareInvitation[];
|
||||
isSafeMode: boolean;
|
||||
enableBetaMarkdownEditor: boolean;
|
||||
enableLegacyMarkdownEditor: boolean;
|
||||
needApiAuth: boolean;
|
||||
processingShareInvitationResponse: boolean;
|
||||
isResettingLayout: boolean;
|
||||
@@ -783,12 +784,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
},
|
||||
|
||||
editor: () => {
|
||||
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror6' : 'TinyMCE';
|
||||
|
||||
if (this.props.isSafeMode) {
|
||||
bodyEditor = 'PlainText';
|
||||
} else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
|
||||
bodyEditor = 'CodeMirror6';
|
||||
} else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) {
|
||||
bodyEditor = 'CodeMirror5';
|
||||
}
|
||||
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
|
||||
},
|
||||
@@ -935,6 +936,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} />
|
||||
{messageComp}
|
||||
{layoutComp}
|
||||
{pluginDialog}
|
||||
@@ -969,7 +971,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
shareInvitations: state.shareService.shareInvitations,
|
||||
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
enableBetaMarkdownEditor: state.settings['editor.beta'],
|
||||
enableLegacyMarkdownEditor: state.settings['editor.legacyMarkdown'],
|
||||
needApiAuth: state.needApiAuth,
|
||||
isResettingLayout: state.isResettingLayout,
|
||||
listRendererId: state.settings['notes.listRendererId'],
|
||||
|
@@ -3,7 +3,7 @@ import shim from '@joplin/lib/shim';
|
||||
import InteropServiceHelper from '../../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'exportPdf',
|
||||
@@ -31,6 +31,14 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(path)) {
|
||||
if (path.length > 1) {
|
||||
throw new Error('Only one output directory can be selected');
|
||||
}
|
||||
|
||||
path = path[0];
|
||||
}
|
||||
|
||||
if (!path) return;
|
||||
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
|
@@ -240,6 +240,6 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} className="master-password-dialog" renderContent={renderDialogWrapper}/>
|
||||
<Dialog onCancel={onClose} className="master-password-dialog">{renderDialogWrapper()}</Dialog>
|
||||
);
|
||||
}
|
||||
|
@@ -713,7 +713,7 @@ function useMenu(props: Props) {
|
||||
label: layoutButtonSequenceOptions[value],
|
||||
type: 'checkbox',
|
||||
click: () => {
|
||||
Setting.setValue('layoutButtonSequence', value);
|
||||
Setting.setValue('layoutButtonSequence', Number(value));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -5,13 +5,13 @@ import DialogButtonRow from './DialogButtonRow';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const Countable = require('@joplin/lib/countable/Countable');
|
||||
import markupLanguageUtils from '../utils/markupLanguageUtils';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
interface NoteContentPropertiesDialogProps {
|
||||
themeId: number;
|
||||
text: string;
|
||||
markupLanguage: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose: Function;
|
||||
onClose: ()=> void;
|
||||
}
|
||||
|
||||
interface TextPropertiesMap {
|
||||
@@ -159,22 +159,20 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
const readTimeLabel = _('Read time: %s min', formatReadTime(strippedReadTime));
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
|
||||
<table>
|
||||
<thead>
|
||||
{tableHeader}
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableBodyComps}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
||||
{readTimeLabel}
|
||||
</div>
|
||||
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
<Dialog onCancel={props.onClose}>
|
||||
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
|
||||
<table>
|
||||
<thead>
|
||||
{tableHeader}
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableBodyComps}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
||||
{readTimeLabel}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
@@ -29,7 +30,14 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
function Toolbar(props: ToolbarProps) {
|
||||
const styles = styles_(props);
|
||||
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={!!props.disabled} />;
|
||||
return (
|
||||
<ToolbarBase
|
||||
style={styles.root}
|
||||
items={props.toolbarButtonInfos}
|
||||
disabled={!!props.disabled}
|
||||
aria-label={_('Editor actions')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
@@ -672,7 +672,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
const percent = getLineScrollPercent();
|
||||
setEditorPercentScroll(percent);
|
||||
options.percent = percent;
|
||||
webviewRef.current.send('setHtml', renderedBody.html, options);
|
||||
webviewRef.current.setHtml(renderedBody.html, options);
|
||||
} else {
|
||||
console.error('Trying to set HTML on an undefined webview ref');
|
||||
}
|
||||
|
@@ -160,7 +160,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
keymapService.on(EventName.KeymapChange, registerKeymap);
|
||||
|
||||
setupEmacs();
|
||||
setupVim(CodeMirror);
|
||||
setupVim(CodeMirror, null);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
@@ -289,7 +289,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
const percent = getLineScrollPercent();
|
||||
setEditorPercentScroll(percent);
|
||||
options.percent = percent;
|
||||
webviewRef.current.send('setHtml', renderedBody.html, options);
|
||||
webviewRef.current.setHtml(renderedBody.html, options);
|
||||
} else {
|
||||
console.error('Trying to set HTML on an undefined webview ref');
|
||||
}
|
||||
@@ -385,6 +385,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
pluginStates={props.plugins}
|
||||
onPasteFile={null}
|
||||
onEvent={onEditorEvent}
|
||||
onLogMessage={logDebug}
|
||||
onEditorPaste={onEditorPaste}
|
||||
|
@@ -12,6 +12,7 @@ import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
@@ -145,7 +146,11 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setupVim(editor);
|
||||
setupVim(editor, {
|
||||
sync: () => {
|
||||
void CommandService.instance().execute('synchronize');
|
||||
},
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
useKeymap(editor);
|
||||
|
@@ -942,6 +942,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
);
|
||||
if (cancelled) return;
|
||||
|
||||
// Use an offset bookmark -- the default bookmark type is not preserved after unloading
|
||||
// and reloading the editor.
|
||||
// See https://github.com/tinymce/tinymce/issues/9736 for a brief description of the
|
||||
// different bookmark types. An offset bookmark seems to have the smallest change
|
||||
// when the note content is updated externally.
|
||||
const offsetBookmarkId = 2;
|
||||
const bookmark = editor.selection.getBookmark(offsetBookmarkId);
|
||||
editor.setContent(awfulInitHack(result.html));
|
||||
|
||||
if (lastOnChangeEventInfo.current.contentKey !== props.contentKey) {
|
||||
@@ -960,6 +967,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// times would result in an empty note.
|
||||
// https://github.com/laurent22/joplin/issues/3534
|
||||
editor.undoManager.reset();
|
||||
} else {
|
||||
// Restore the cursor location
|
||||
editor.selection.bookmarkManager.moveToBookmark(bookmark);
|
||||
}
|
||||
|
||||
lastOnChangeEventInfo.current = {
|
||||
|
@@ -35,7 +35,6 @@ import NoteSearchBar from '../NoteSearchBar';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import bridge from '../../services/bridge';
|
||||
import NoteRevisionViewer from '../NoteRevisionViewer';
|
||||
import { parseShareCache } from '@joplin/lib/services/share/reducer';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
@@ -51,6 +50,7 @@ import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginS
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
||||
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
||||
import WarningBanner from './WarningBanner/WarningBanner';
|
||||
const debounce = require('debounce');
|
||||
|
||||
const commands = [
|
||||
@@ -138,7 +138,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
@@ -434,7 +434,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'PlainText') {
|
||||
editor = <PlainEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror') {
|
||||
} else if (props.bodyEditor === 'CodeMirror5') {
|
||||
editor = <CodeMirror5 {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror6') {
|
||||
editor = <CodeMirror6 {...editorProps}/>;
|
||||
@@ -442,22 +442,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||
}
|
||||
|
||||
const onRichTextReadMoreLinkClick = useCallback(() => {
|
||||
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
|
||||
}, []);
|
||||
|
||||
const onRichTextDismissLinkClick = useCallback(() => {
|
||||
Setting.setValue('richTextBannerDismissed', true);
|
||||
}, []);
|
||||
|
||||
const wysiwygBanner = props.bodyEditor !== 'TinyMCE' || props.richTextBannerDismissed ? null : (
|
||||
<div style={styles.warningBanner}>
|
||||
{_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')}
|
||||
<a onClick={onRichTextReadMoreLinkClick} style={styles.warningBannerLink} href="#">[ {_('Read more about it')} ]</a>
|
||||
<a onClick={onRichTextDismissLinkClick} style={styles.warningBannerLink} href="#">[ {_('Dismiss')} ]</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const noteRevisionViewer_onBack = useCallback(() => {
|
||||
setShowRevisions(false);
|
||||
}, []);
|
||||
@@ -612,7 +596,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
{renderTagButton()}
|
||||
{renderTagBar()}
|
||||
</div>
|
||||
{wysiwygBanner}
|
||||
<WarningBanner bodyEditor={props.bodyEditor}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -636,7 +620,6 @@ const mapStateToProps = (state: AppState) => {
|
||||
syncStarted: state.syncStarted,
|
||||
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||
themeId: state.settings.theme,
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedNoteTags: state.selectedNoteTags,
|
||||
|
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
acceptMessage: string;
|
||||
onAccept: ()=> void;
|
||||
onDismiss: ()=> void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const BannerContent: React.FC<Props> = props => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className='warning-banner'>
|
||||
{props.children}
|
||||
<a onClick={props.onAccept} className='warning-banner-link' href="#">[ {props.acceptMessage} ]</a>
|
||||
<a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {_('Dismiss')} ]</a>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default BannerContent;
|
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import BannerContent from './BannerContent';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
|
||||
interface Props {
|
||||
bodyEditor: string;
|
||||
richTextBannerDismissed: boolean;
|
||||
pluginCompatibilityBannerDismissedFor: string[];
|
||||
plugins: PluginStates;
|
||||
}
|
||||
|
||||
const onRichTextDismissLinkClick = () => {
|
||||
Setting.setValue('richTextBannerDismissed', true);
|
||||
};
|
||||
|
||||
const onRichTextReadMoreLinkClick = () => {
|
||||
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
|
||||
};
|
||||
|
||||
const onSwitchToLegacyEditor = () => {
|
||||
Setting.setValue('editor.legacyMarkdown', true);
|
||||
};
|
||||
|
||||
const onDismissLegacyEditorPrompt = () => {
|
||||
Setting.setValue('editor.pluginCompatibilityBannerDismissedFor', [...PluginService.instance().pluginIds]);
|
||||
};
|
||||
|
||||
const incompatiblePluginIds = [
|
||||
// cSpell:disable
|
||||
'com.septemberhx.Joplin.Enhancement',
|
||||
'ylc395.noteLinkSystem',
|
||||
'outline',
|
||||
'joplin.plugin.cmoptions',
|
||||
'plugin.calebjohn.MathMode',
|
||||
'com.ckant.joplin-plugin-better-code-blocks',
|
||||
// cSpell:enable
|
||||
];
|
||||
|
||||
const WarningBanner: React.FC<Props> = props => {
|
||||
const wysiwygBanner = (
|
||||
<BannerContent
|
||||
acceptMessage={_('Read more about it')}
|
||||
onAccept={onRichTextReadMoreLinkClick}
|
||||
onDismiss={onRichTextDismissLinkClick}
|
||||
visible={props.bodyEditor === 'TinyMCE' && !props.richTextBannerDismissed}
|
||||
>
|
||||
{_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')}
|
||||
</BannerContent>
|
||||
);
|
||||
|
||||
const incompatiblePluginNames = useMemo(() => {
|
||||
if (props.bodyEditor !== 'CodeMirror6') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const runningPluginIds = Object.keys(props.plugins);
|
||||
|
||||
return runningPluginIds.map((id): string|string[] => {
|
||||
if (props.pluginCompatibilityBannerDismissedFor?.includes(id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (incompatiblePluginIds.includes(id)) {
|
||||
return PluginService.instance().pluginById(id).manifest.name;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}).flat();
|
||||
}, [props.bodyEditor, props.plugins, props.pluginCompatibilityBannerDismissedFor]);
|
||||
|
||||
const markdownPluginBanner = (
|
||||
<BannerContent
|
||||
acceptMessage={_('Switch to the legacy editor')}
|
||||
onAccept={onSwitchToLegacyEditor}
|
||||
onDismiss={onDismissLegacyEditorPrompt}
|
||||
visible={incompatiblePluginNames.length > 0}
|
||||
>
|
||||
{_('The following plugins may not support the current markdown editor:')}
|
||||
<ul>
|
||||
{incompatiblePluginNames.map((name, index) => <li key={index}>{name}</li>)}
|
||||
</ul>
|
||||
</BannerContent>
|
||||
);
|
||||
|
||||
return <>
|
||||
{wysiwygBanner}
|
||||
{markdownPluginBanner}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
pluginCompatibilityBannerDismissedFor: state.settings['editor.pluginCompatibilityBannerDismissedFor'],
|
||||
plugins: state.pluginService.plugins,
|
||||
};
|
||||
})(WarningBanner);
|
3
packages/app-desktop/gui/NoteEditor/style.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
@use "./styles/warning-banner.scss";
|
||||
@use "./styles/warning-banner-link.scss";
|
@@ -0,0 +1,6 @@
|
||||
.warning-banner-link {
|
||||
color: var(--joplin-color);
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-siize);
|
||||
font-weight: bold;
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
|
||||
.warning-banner {
|
||||
background: var(--joplin-warning-background-color);
|
||||
font-family: var(--joplin-font-family);
|
||||
padding: 10px;
|
||||
font-size: var(--joplin-font-size);
|
||||
line-height: 1.6em;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
max-height: 25vh;
|
||||
overflow-y: auto;
|
||||
}
|
@@ -150,7 +150,9 @@ export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, m
|
||||
bridge().showInfoMessageBox(_('This attachment does not have OCR data (Status: %s)', resourceOcrStatusToString(resource.ocr_status)));
|
||||
}
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, _options: ContextMenuOptions) => itemType === ContextMenuItemType.Resource,
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => {
|
||||
return itemType === ContextMenuItemType.Resource || (itemType === ContextMenuItemType.Image && options.resourceId);
|
||||
},
|
||||
},
|
||||
copyPathToClipboard: {
|
||||
label: _('Copy path to clipboard'),
|
||||
|
@@ -50,7 +50,6 @@ export interface NoteEditorProps {
|
||||
plugins: PluginStates;
|
||||
toolbarButtonInfos: ToolbarButtonInfo[];
|
||||
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
||||
richTextBannerDismissed: boolean;
|
||||
contentMaxWidth: number;
|
||||
isSafeMode: boolean;
|
||||
useCustomPdfViewer: boolean;
|
||||
|
@@ -22,7 +22,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
|
||||
const markupToHtml = useMemo(() => {
|
||||
return markupLanguageUtils.newMarkupToHtml(plugins, {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
|
||||
customCss: customCss || '',
|
||||
});
|
||||
}, [plugins, customCss]);
|
||||
|
@@ -104,10 +104,17 @@ const useOnKeyDown = (
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
if (noteIds.length) {
|
||||
if (key === 'Delete' && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
|
||||
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
|
||||
}
|
||||
} else if (key === 'Delete' || (key === 'Backspace' && event.metaKey)) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,14 +8,14 @@ import bridge from '../services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from './Dialog';
|
||||
const Datetime = require('react-datetime').default;
|
||||
const { clipboard } = require('electron');
|
||||
const formatcoords = require('formatcoords');
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose: Function;
|
||||
onClose: ()=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onRevisionLinkClick: Function;
|
||||
themeId: number;
|
||||
@@ -174,7 +174,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
textDecoration: 'none',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
padding: '.14em',
|
||||
display: 'flex',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '0.5em',
|
||||
@@ -281,11 +281,13 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
public createNoteField(key: keyof FormNote, value: any) {
|
||||
const styles = this.styles(this.props.themeId);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const labelComp = <label style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{this.formatLabel(key)}</label>;
|
||||
const labelText = this.formatLabel(key);
|
||||
const labelComp = <label role='rowheader' style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{labelText}</label>;
|
||||
let controlComp = null;
|
||||
let editComp = null;
|
||||
let editCompHandler = null;
|
||||
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) => {
|
||||
@@ -320,6 +322,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
void this.saveProperty();
|
||||
};
|
||||
editCompIcon = 'fa-save';
|
||||
editComDescription = _('Save changes');
|
||||
} else {
|
||||
controlComp = (
|
||||
<input
|
||||
@@ -374,28 +377,35 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
this.editPropertyButtonClick(key, value);
|
||||
};
|
||||
editCompIcon = 'fa-edit';
|
||||
editComDescription = _('Edit');
|
||||
}
|
||||
|
||||
// Add the copy icon and the 'copy on click' event
|
||||
if (key === 'id') {
|
||||
editCompIcon = 'fa-copy';
|
||||
editCompHandler = () => clipboard.writeText(value);
|
||||
editComDescription = _('Copy');
|
||||
}
|
||||
}
|
||||
|
||||
if (editCompHandler && !this.isReadOnly()) {
|
||||
editComp = (
|
||||
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={editCompHandler}
|
||||
style={styles.editPropertyButton}
|
||||
aria-label={editComDescription}
|
||||
title={editComDescription}
|
||||
>
|
||||
<i className={`fas ${editCompIcon}`} aria-hidden="true"></i>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} style={theme.controlBox} className="note-property-box">
|
||||
<div role='row' key={key} style={theme.controlBox} className="note-property-box">
|
||||
{labelComp}
|
||||
{controlComp}
|
||||
{editComp}
|
||||
<span role='cell'>{controlComp} {editComp}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -437,13 +447,13 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||
<div>{noteComps}</div>
|
||||
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
<Dialog onCancel={this.props.onClose}>
|
||||
<div style={theme.dialogTitle} id='note-properties-dialog-title'>{_('Note properties')}</div>
|
||||
<div role='table' aria-labelledby='note-properties-dialog-title'>
|
||||
{noteComps}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -140,7 +140,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
|
||||
customCss: this.props.customCss ? this.props.customCss : '',
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
});
|
||||
|
||||
this.viewerRef_.current.send('setHtml', result.html, {
|
||||
this.viewerRef_.current.setHtml(result.html, {
|
||||
// cssFiles: result.cssFiles,
|
||||
pluginAssets: result.pluginAssets,
|
||||
});
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
|
||||
import * as React from 'react';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import bridge from '../services/bridge';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface Props {
|
||||
@@ -14,6 +15,12 @@ interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
type RemovePluginAssetsCallback = ()=> void;
|
||||
|
||||
interface SetHtmlOptions {
|
||||
pluginAssets: { path: string }[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default class NoteTextViewerComponent extends React.Component<Props, any> {
|
||||
|
||||
@@ -23,6 +30,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
private webviewRef_: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webviewListeners_: any = null;
|
||||
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(props: any) {
|
||||
@@ -64,8 +72,8 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
this.webview_domReady({});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webview_message(event: any) {
|
||||
private webview_message(event: MessageEvent) {
|
||||
if (event.source !== this.webviewRef_.current?.contentWindow) return;
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
@@ -100,7 +108,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
wv.addEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
|
||||
window.addEventListener('message', this.webview_message);
|
||||
}
|
||||
|
||||
public destroyWebview() {
|
||||
@@ -113,17 +121,12 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
wv.removeEventListener(n, fn);
|
||||
}
|
||||
|
||||
try {
|
||||
// It seems this can throw a cross-origin error in a way that is hard to replicate so just wrap
|
||||
// it in try/catch since it's not critical.
|
||||
// https://github.com/laurent22/joplin/issues/3835
|
||||
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error destroying note viewer', error);
|
||||
}
|
||||
window.removeEventListener('message', this.webview_message);
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
|
||||
this.removePluginAssetsCallback_?.();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
@@ -163,6 +166,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
|
||||
}
|
||||
|
||||
// External code should use .setHtml (rather than send('setHtml', ...))
|
||||
if (channel === 'setHtml') {
|
||||
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
@@ -180,12 +184,48 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public setHtml(html: string, options: SetHtmlOptions) {
|
||||
// Grant & remove asset access.
|
||||
if (options.pluginAssets) {
|
||||
this.removePluginAssetsCallback_?.();
|
||||
|
||||
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
||||
|
||||
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
|
||||
const assetAccesses = pluginAssetPaths.map(
|
||||
path => protocolHandler.allowReadAccessToFile(path),
|
||||
);
|
||||
|
||||
this.removePluginAssetsCallback_ = () => {
|
||||
for (const accessControl of assetAccesses) {
|
||||
accessControl.remove();
|
||||
}
|
||||
|
||||
this.removePluginAssetsCallback_ = null;
|
||||
};
|
||||
}
|
||||
|
||||
this.send('setHtml', html, options);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Wrap WebView functions (END)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public render() {
|
||||
const viewerStyle = { border: 'none', ...this.props.viewerStyle };
|
||||
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
|
||||
|
||||
// allow=fullscreen: Required to allow the user to fullscreen videos.
|
||||
return (
|
||||
<iframe
|
||||
className="noteTextViewer"
|
||||
ref={this.webviewRef_}
|
||||
style={viewerStyle}
|
||||
allow='fullscreen=* autoplay=* local-fonts=* encrypted-media=*'
|
||||
allowFullScreen={true}
|
||||
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/comm
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
const { connect } = require('react-redux');
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface NoteToolbarProps {
|
||||
themeId: number;
|
||||
@@ -29,7 +30,14 @@ function styles_(props: NoteToolbarProps) {
|
||||
|
||||
function NoteToolbar(props: NoteToolbarProps) {
|
||||
const styles = styles_(props);
|
||||
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={props.disabled}/>;
|
||||
return (
|
||||
<ToolbarBase
|
||||
style={styles.root}
|
||||
items={props.toolbarButtonInfos}
|
||||
disabled={props.disabled}
|
||||
aria-label={_('Note')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
@@ -7,6 +7,8 @@ import CreatableSelect from 'react-select/creatable';
|
||||
import Select from 'react-select';
|
||||
import makeAnimated from 'react-select/animated';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -90,31 +92,6 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const paddingTop = 20;
|
||||
|
||||
this.styles_.modalLayer = {
|
||||
zIndex: 9999,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
display: visible ? 'flex' : 'none',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
paddingTop: `${paddingTop}px`,
|
||||
};
|
||||
|
||||
this.styles_.promptDialog = {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
padding: 16,
|
||||
display: 'inline-block',
|
||||
maxWidth: width * 0.5,
|
||||
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
||||
};
|
||||
|
||||
this.styles_.button = {
|
||||
minWidth: theme.buttonMinWidth,
|
||||
minHeight: theme.buttonMinHeight,
|
||||
@@ -214,10 +191,14 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
|
||||
this.styles_.desc = { ...theme.textStyle, marginTop: 10 };
|
||||
|
||||
this.styles_.dialog = { maxWidth: width };
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.state.visible) return null;
|
||||
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
|
||||
@@ -325,16 +306,14 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-layer" style={styles.modalLayer}>
|
||||
<div className="modal-dialog" style={styles.promptDialog}>
|
||||
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
|
||||
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
|
||||
{inputComp}
|
||||
{descComp}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
|
||||
<Dialog className='prompt-dialog' contentStyle={styles.dialog}>
|
||||
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
|
||||
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
|
||||
{inputComp}
|
||||
{descComp}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -167,7 +167,7 @@ class RootComponent extends React.Component<Props, any> {
|
||||
);
|
||||
};
|
||||
|
||||
return <Dialog renderContent={renderContent}/>;
|
||||
return <Dialog>{renderContent()}</Dialog>;
|
||||
}
|
||||
|
||||
private modalDialogProps(): ModalDialogProps {
|
||||
|
@@ -407,7 +407,7 @@ function ShareFolderDialog(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog renderContent={renderContent}/>
|
||||
<Dialog>{renderContent()}</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -226,7 +226,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog renderContent={renderContent}/>
|
||||
<Dialog>{renderContent()}</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagListItem } from '../types';
|
||||
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -35,10 +35,13 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
});
|
||||
}, [props.tags]);
|
||||
|
||||
const folderTree = useMemo(() => {
|
||||
return buildFolderTree(props.folders);
|
||||
}, [props.folders]);
|
||||
|
||||
const folderItems = useMemo(() => {
|
||||
const renderProps = {
|
||||
folders: props.folders,
|
||||
folderTree,
|
||||
collapsedFolderIds: props.collapsedFolderIds,
|
||||
};
|
||||
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
|
||||
@@ -50,7 +53,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
key: folder.id,
|
||||
};
|
||||
});
|
||||
}, [props.folders, props.collapsedFolderIds]);
|
||||
}, [folderTree, props.collapsedFolderIds]);
|
||||
|
||||
return useMemo(() => {
|
||||
const foldersHeader: HeaderListItem = {
|
||||
|
@@ -2,12 +2,14 @@ import * as React from 'react';
|
||||
|
||||
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import ExpandLink from './ExpandLink';
|
||||
import { StyledListItem, StyledListItemAnchor, StyledNoteCount, StyledShareIcon, StyledSpanFix } from '../styles';
|
||||
import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles';
|
||||
import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types';
|
||||
import FolderIconBox from '../../FolderIconBox';
|
||||
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import NoteCount from './NoteCount';
|
||||
|
||||
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
||||
if (!folderIcon) {
|
||||
@@ -47,8 +49,8 @@ interface FolderItemProps {
|
||||
function FolderItem(props: FolderItemProps) {
|
||||
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
||||
|
||||
const noteCountComp = noteCount ? <StyledNoteCount className="note-count-label">{noteCount}</StyledNoteCount> : null;
|
||||
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null;
|
||||
const shareTitle = _('Shared');
|
||||
const shareIcon = shareId && !parentId ? <StyledShareIcon aria-label={shareTitle} title={shareTitle} className="fas fa-share-alt"/> : null;
|
||||
const draggable = ![getTrashFolderId(), Folder.conflictFolderId()].includes(folderId);
|
||||
|
||||
const doRenderFolderIcon = () => {
|
||||
@@ -69,6 +71,7 @@ function FolderItem(props: FolderItemProps) {
|
||||
isConflictFolder={folderId === Folder.conflictFolderId()}
|
||||
href="#"
|
||||
selected={selected}
|
||||
aria-selected={selected}
|
||||
shareId={shareId}
|
||||
data-id={folderId}
|
||||
data-type={ModelType.Folder}
|
||||
@@ -80,7 +83,7 @@ function FolderItem(props: FolderItemProps) {
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
|
||||
{shareIcon} {noteCountComp}
|
||||
{shareIcon} <NoteCount count={noteCount}/>
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
|
@@ -61,7 +61,7 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
tabIndex={0}
|
||||
ref={props.anchorRef}
|
||||
>
|
||||
<StyledHeaderIcon className={item.iconName}/>
|
||||
<StyledHeaderIcon aria-label='' className={item.iconName}/>
|
||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||
</StyledHeader>
|
||||
{ item.onPlusButtonClick && addButton }
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { StyledNoteCount } from '../styles';
|
||||
import { _n } from '@joplin/lib/locale';
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -8,7 +8,8 @@ interface Props {
|
||||
|
||||
const NoteCount: React.FC<Props> = props => {
|
||||
const count = props.count;
|
||||
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
|
||||
const title = _n('Contains %d note', 'Contains %d notes', count, count);
|
||||
return count ? <div role='note' aria-label={title} title={title} className="note-count-label">{count}</div> : null;
|
||||
};
|
||||
|
||||
export default NoteCount;
|
||||
|
@@ -33,10 +33,12 @@ const TagItem = (props: Props) => {
|
||||
}, [props.onClick, tag]);
|
||||
|
||||
return (
|
||||
<StyledListItem selected={selected}
|
||||
<StyledListItem
|
||||
selected={selected}
|
||||
className={`list-item-container ${selected ? 'selected' : ''}`}
|
||||
onDrop={props.onTagDrop}
|
||||
data-tag-id={tag.id}
|
||||
aria-selected={selected}
|
||||
>
|
||||
<EmptyExpandLink/>
|
||||
<StyledListItemAnchor
|
||||
|
@@ -1,4 +1,5 @@
|
||||
@use 'styles/folder-and-tag-list.scss';
|
||||
@use 'styles/note-count-label.scss';
|
||||
@use 'styles/sidebar-expand-icon.scss';
|
||||
@use 'styles/sidebar-expand-link.scss';
|
||||
@use 'styles/sidebar-header-container.scss';
|
||||
|
@@ -95,12 +95,6 @@ export const StyledShareIcon = styled.i`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export const StyledNoteCount = styled.div`
|
||||
color: ${(props: StyleProps) => props.theme.colorFaded2};
|
||||
padding-left: 8px;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const StyledSynchronizeButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
@@ -0,0 +1,6 @@
|
||||
|
||||
.note-count-label {
|
||||
color: var(--joplin-color-faded2);
|
||||
padding-left: 8px;
|
||||
user-select: none;
|
||||
}
|
@@ -140,17 +140,16 @@ type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
|
||||
export default function(props: Props) {
|
||||
const joplinCloudDescriptionRef = useRef(null);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function closeDialog(dispatch: Function) {
|
||||
dispatch({
|
||||
const closeDialog = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onButtonRowClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
}, [props.dispatch]);
|
||||
closeDialog();
|
||||
}, [closeDialog]);
|
||||
|
||||
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
|
||||
|
||||
@@ -184,12 +183,12 @@ export default function(props: Props) {
|
||||
|
||||
Setting.setValue('sync.target', route.target);
|
||||
await Setting.saveAll();
|
||||
closeDialog(props.dispatch);
|
||||
closeDialog();
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: route.name,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
}, [props.dispatch, closeDialog]);
|
||||
|
||||
function renderSelectArea(info: SyncTargetInfo) {
|
||||
return (
|
||||
@@ -229,7 +228,7 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
const onSelfHostingClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
closeDialog();
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -238,7 +237,7 @@ export default function(props: Props) {
|
||||
defaultSection: 'sync',
|
||||
},
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
}, [props.dispatch, closeDialog]);
|
||||
|
||||
function renderContent() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -278,6 +277,6 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog renderContent={renderDialogWrapper}/>
|
||||
<Dialog onCancel={closeDialog}>{renderDialogWrapper()}</Dialog>
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import styles_ from './styles';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export enum Value {
|
||||
Markdown = 'markdown',
|
||||
@@ -11,6 +12,8 @@ export interface Props {
|
||||
themeId: number;
|
||||
value: Value;
|
||||
toolbarButtonInfo: ToolbarButtonInfo;
|
||||
tabIndex?: number;
|
||||
buttonRef?: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export default function ToggleEditorsButton(props: Props) {
|
||||
@@ -18,14 +21,17 @@ export default function ToggleEditorsButton(props: Props) {
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={props.buttonRef}
|
||||
style={style.button}
|
||||
disabled={!props.toolbarButtonInfo.enabled}
|
||||
aria-label={props.toolbarButtonInfo.tooltip}
|
||||
aria-description={_('Switch to the %s Editor', props.value !== Value.Markdown ? _('Markdown') : _('Rich Text'))}
|
||||
title={props.toolbarButtonInfo.tooltip}
|
||||
type="button"
|
||||
className={`tox-tbtn ${props.value}-active`}
|
||||
aria-pressed="false"
|
||||
onClick={props.toolbarButtonInfo.onClick}
|
||||
tabIndex={props.tabIndex}
|
||||
>
|
||||
<div style={style.leftInnerButton}>
|
||||
<i style={style.leftIcon} className="fab fa-markdown"></i>
|
||||
|
@@ -2,98 +2,207 @@ import * as React from 'react';
|
||||
import ToolbarButton from './ToolbarButton/ToolbarButton';
|
||||
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
|
||||
import ToolbarSpace from './ToolbarSpace';
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface ToolbarItemInfo extends ToolbarButtonInfo {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
items: any[];
|
||||
style: React.CSSProperties;
|
||||
items: ToolbarItemInfo[];
|
||||
disabled: boolean;
|
||||
'aria-label': string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
class ToolbarBaseComponent extends React.Component<Props, any> {
|
||||
const getItemType = (item: ToolbarItemInfo) => {
|
||||
return item.type ?? 'button';
|
||||
};
|
||||
|
||||
public render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const isFocusable = (item: ToolbarItemInfo) => {
|
||||
if (!item.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const style: any = { display: 'flex',
|
||||
flexDirection: 'row',
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
padding: theme.toolbarPadding,
|
||||
paddingRight: theme.mainPadding, ...this.props.style };
|
||||
return getItemType(item) === 'button';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const groupStyle: any = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
boxSizing: 'border-box',
|
||||
minWidth: 0,
|
||||
};
|
||||
const useCategorizedItems = (items: ToolbarItemInfo[]) => {
|
||||
return useMemo(() => {
|
||||
const itemsLeft: ToolbarItemInfo[] = [];
|
||||
const itemsCenter: ToolbarItemInfo[] = [];
|
||||
const itemsRight: ToolbarItemInfo[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const leftItemComps: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const centerItemComps: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const rightItemComps: any[] = [];
|
||||
|
||||
if (this.props.items) {
|
||||
for (let i = 0; i < this.props.items.length; i++) {
|
||||
const o = this.props.items[i];
|
||||
let key = o.iconName ? o.iconName : '';
|
||||
key += o.title ? o.title : '';
|
||||
key += o.name ? o.name : '';
|
||||
const itemType = !('type' in o) ? 'button' : o.type;
|
||||
|
||||
if (!key) key = `${o.type}_${i}`;
|
||||
|
||||
const props = {
|
||||
key: key,
|
||||
themeId: this.props.themeId,
|
||||
disabled: this.props.disabled,
|
||||
...o,
|
||||
};
|
||||
|
||||
if (o.name === 'toggleEditors') {
|
||||
rightItemComps.push(<ToggleEditorsButton
|
||||
key={o.name}
|
||||
value={Value.Markdown}
|
||||
themeId={this.props.themeId}
|
||||
toolbarButtonInfo={o}
|
||||
/>);
|
||||
} else if (itemType === 'button') {
|
||||
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps;
|
||||
target.push(<ToolbarButton {...props} />);
|
||||
} else if (itemType === 'separator') {
|
||||
centerItemComps.push(<ToolbarSpace {...props} />);
|
||||
if (items) {
|
||||
for (const item of items) {
|
||||
const type = getItemType(item);
|
||||
if (item.name === 'toggleEditors') {
|
||||
itemsRight.push(item);
|
||||
} else if (type === 'button') {
|
||||
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(item.name) ? itemsLeft : itemsCenter;
|
||||
target.push(item);
|
||||
} else if (type === 'separator') {
|
||||
itemsCenter.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-toolbar" style={style}>
|
||||
<div style={groupStyle}>
|
||||
{leftItemComps}
|
||||
</div>
|
||||
<div style={groupStyle}>
|
||||
{centerItemComps}
|
||||
</div>
|
||||
<div style={{ ...groupStyle, flex: 1, justifyContent: 'flex-end' }}>
|
||||
{rightItemComps}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
itemsLeft,
|
||||
itemsCenter,
|
||||
itemsRight,
|
||||
allItems: itemsLeft.concat(itemsCenter, itemsRight),
|
||||
};
|
||||
}, [items]);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mapStateToProps = (state: any) => {
|
||||
const useKeyboardHandler = (
|
||||
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>,
|
||||
focusableItems: ToolbarItemInfo[],
|
||||
) => {
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => {
|
||||
let direction = 0;
|
||||
if (event.code === 'ArrowRight') {
|
||||
direction = 1;
|
||||
} else if (event.code === 'ArrowLeft') {
|
||||
direction = -1;
|
||||
}
|
||||
|
||||
let handled = true;
|
||||
if (direction !== 0) {
|
||||
setSelectedIndex(index => {
|
||||
let newIndex = (index + direction) % focusableItems.length;
|
||||
if (newIndex < 0) {
|
||||
newIndex += focusableItems.length;
|
||||
}
|
||||
return newIndex;
|
||||
});
|
||||
} else if (event.code === 'End') {
|
||||
setSelectedIndex(focusableItems.length - 1);
|
||||
} else if (event.code === 'Home') {
|
||||
setSelectedIndex(0);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [focusableItems, setSelectedIndex]);
|
||||
|
||||
return onKeyDown;
|
||||
};
|
||||
|
||||
const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
const { itemsLeft, itemsCenter, itemsRight, allItems } = useCategorizedItems(props.items);
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const focusableItems = useMemo(() => {
|
||||
return allItems.filter(isFocusable);
|
||||
}, [allItems]);
|
||||
const containerRef = useRef<HTMLDivElement|null>(null);
|
||||
const containerHasFocus = !!containerRef.current?.contains(document.activeElement);
|
||||
|
||||
let keyCounter = 0;
|
||||
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => {
|
||||
let key = o.iconName ? o.iconName : '';
|
||||
key += o.title ? o.title : '';
|
||||
key += o.name ? o.name : '';
|
||||
const itemType = !('type' in o) ? 'button' : o.type;
|
||||
|
||||
if (!key) key = `${o.type}_${keyCounter++}`;
|
||||
|
||||
const buttonProps = {
|
||||
key,
|
||||
themeId: props.themeId,
|
||||
disabled: props.disabled || !o.enabled,
|
||||
...o,
|
||||
};
|
||||
|
||||
const tabIndex = indexInFocusable === (selectedIndex % focusableItems.length) ? 0 : -1;
|
||||
const setButtonRefCallback = (button: HTMLButtonElement) => {
|
||||
if (tabIndex === 0 && containerHasFocus) {
|
||||
focus('ToolbarBase', button);
|
||||
}
|
||||
};
|
||||
|
||||
if (o.name === 'toggleEditors') {
|
||||
return <ToggleEditorsButton
|
||||
key={o.name}
|
||||
buttonRef={setButtonRefCallback}
|
||||
value={Value.Markdown}
|
||||
themeId={props.themeId}
|
||||
toolbarButtonInfo={o}
|
||||
tabIndex={tabIndex}
|
||||
/>;
|
||||
} else if (itemType === 'button') {
|
||||
return (
|
||||
<ToolbarButton
|
||||
tabIndex={tabIndex}
|
||||
buttonRef={setButtonRefCallback}
|
||||
{...buttonProps}
|
||||
/>
|
||||
);
|
||||
} else if (itemType === 'separator') {
|
||||
return <ToolbarSpace {...buttonProps} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
let focusableIndex = 0;
|
||||
const renderList = (items: ToolbarItemInfo[]) => {
|
||||
const result: React.ReactNode[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
result.push(renderItem(item, focusableIndex));
|
||||
if (isFocusable(item)) {
|
||||
focusableIndex ++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const leftItemComps = renderList(itemsLeft);
|
||||
const centerItemComps = renderList(itemsCenter);
|
||||
const rightItemComps = renderList(itemsRight);
|
||||
|
||||
const onKeyDown = useKeyboardHandler(
|
||||
setSelectedIndex,
|
||||
focusableItems,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='editor-toolbar'
|
||||
style={props.style}
|
||||
|
||||
role='toolbar'
|
||||
aria-label={props['aria-label']}
|
||||
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className='group'>
|
||||
{leftItemComps}
|
||||
</div>
|
||||
<div className='group'>
|
||||
{centerItemComps}
|
||||
</div>
|
||||
<div className='group -right'>
|
||||
{rightItemComps}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return { themeId: state.settings.theme };
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { StyledRoot, StyledIconSpan, StyledIconI } from './styles';
|
||||
import { StyledIconSpan, StyledIconI } from './styles';
|
||||
|
||||
interface Props {
|
||||
readonly themeId: number;
|
||||
@@ -10,6 +10,9 @@ interface Props {
|
||||
readonly iconName?: string;
|
||||
readonly disabled?: boolean;
|
||||
readonly backgroundHover?: boolean;
|
||||
readonly tabIndex?: number;
|
||||
|
||||
buttonRef?: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function isFontAwesomeIcon(iconName: string) {
|
||||
@@ -34,7 +37,7 @@ export default function ToolbarButton(props: Props) {
|
||||
const iconName = getProp(props, 'iconName');
|
||||
if (iconName) {
|
||||
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
|
||||
icon = <IconClass className={iconName} title={title}/>;
|
||||
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
|
||||
}
|
||||
|
||||
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
||||
@@ -42,28 +45,37 @@ export default function ToolbarButton(props: Props) {
|
||||
if (isEnabled === null) isEnabled = true;
|
||||
if (props.disabled) isEnabled = false;
|
||||
|
||||
const classes = ['button'];
|
||||
const classes = ['button', 'toolbar-button'];
|
||||
if (!isEnabled) classes.push('disabled');
|
||||
if (title) classes.push('-has-title');
|
||||
|
||||
const onClick = getProp(props, 'onClick');
|
||||
const style: React.CSSProperties = {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis' };
|
||||
const disabled = !isEnabled;
|
||||
|
||||
return (
|
||||
<StyledRoot
|
||||
<button
|
||||
className={classes.join(' ')}
|
||||
disabled={!isEnabled}
|
||||
title={tooltip}
|
||||
href="#"
|
||||
hasTitle={!!title}
|
||||
onClick={() => {
|
||||
if (isEnabled && onClick) onClick();
|
||||
}}
|
||||
ref={props.buttonRef}
|
||||
|
||||
// At least on MacOS, the disabled HTML prop isn't sufficient for the screen reader
|
||||
// to read the element as disable. For this, aria-disabled is necessary.
|
||||
disabled={disabled}
|
||||
aria-label={!title ? tooltip : undefined}
|
||||
aria-description={title ? tooltip : undefined}
|
||||
aria-disabled={!isEnabled}
|
||||
tabIndex={props.tabIndex}
|
||||
>
|
||||
{icon}
|
||||
<span style={style}>{title}</span>
|
||||
</StyledRoot>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -3,46 +3,15 @@ import { ThemeStyle } from '@joplin/lib/theme';
|
||||
const styled = require('styled-components').default;
|
||||
const { css } = require('styled-components');
|
||||
|
||||
interface RootProps {
|
||||
readonly theme: ThemeStyle;
|
||||
readonly disabled: boolean;
|
||||
readonly hasTitle: boolean;
|
||||
}
|
||||
|
||||
export const StyledRoot = styled.a<RootProps>`
|
||||
opacity: ${(props: RootProps) => props.disabled ? 0.3 : 1};
|
||||
height: ${(props: RootProps) => props.theme.toolbarHeight}px;
|
||||
min-height: ${(props: RootProps) => props.theme.toolbarHeight}px;
|
||||
width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
|
||||
max-width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
color: ${(props: RootProps) => props.theme.color3};
|
||||
font-size: ${(props: RootProps) => props.theme.toolbarIconSize * 0.8}px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props: RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3};
|
||||
}
|
||||
`;
|
||||
|
||||
interface IconProps {
|
||||
readonly theme: ThemeStyle;
|
||||
readonly title: string;
|
||||
readonly hasTitle: boolean;
|
||||
}
|
||||
|
||||
const iconStyle = css<IconProps>`
|
||||
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
|
||||
color: ${(props: IconProps) => props.theme.color3};
|
||||
margin-right: ${(props: IconProps) => props.title ? 5 : 0}px;
|
||||
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
|
||||
pointer-events: none; /* Need this to get button tooltip to work */
|
||||
`;
|
||||
|
||||
|
@@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UpdateNotificationEvents = void 0;
|
||||
const React = require("react");
|
||||
const react_1 = require("react");
|
||||
const theme_1 = require("@joplin/lib/theme");
|
||||
const NotyfContext_1 = require("../NotyfContext");
|
||||
const electron_1 = require("electron");
|
||||
const AutoUpdaterService_1 = require("../../services/autoUpdater/AutoUpdaterService");
|
||||
const locale_1 = require("@joplin/lib/locale");
|
||||
const html_1 = require("@joplin/utils/html");
|
||||
var UpdateNotificationEvents;
|
||||
(function (UpdateNotificationEvents) {
|
||||
UpdateNotificationEvents["ApplyUpdate"] = "apply-update";
|
||||
UpdateNotificationEvents["Dismiss"] = "dismiss-update-notification";
|
||||
})(UpdateNotificationEvents || (exports.UpdateNotificationEvents = UpdateNotificationEvents = {}));
|
||||
const changelogLink = 'https://github.com/laurent22/joplin/releases';
|
||||
window.openChangelogLink = () => {
|
||||
electron_1.ipcRenderer.send('open-link', changelogLink);
|
||||
};
|
||||
const UpdateNotification = ({ themeId }) => {
|
||||
const notyfContext = (0, react_1.useContext)(NotyfContext_1.default);
|
||||
const notificationRef = (0, react_1.useRef)(null); // Use ref to hold the current notification
|
||||
const theme = (0, react_1.useMemo)(() => (0, theme_1.themeStyle)(themeId), [themeId]);
|
||||
const notyf = (0, react_1.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.color = theme.backgroundColor5;
|
||||
}
|
||||
return type;
|
||||
});
|
||||
return output;
|
||||
}, [notyfContext, theme]);
|
||||
const handleDismissNotification = (0, react_1.useCallback)(() => {
|
||||
notyf.dismiss(notificationRef.current);
|
||||
notificationRef.current = null;
|
||||
}, [notyf]);
|
||||
const handleApplyUpdate = (0, react_1.useCallback)(() => {
|
||||
electron_1.ipcRenderer.send('apply-update-now');
|
||||
handleDismissNotification();
|
||||
}, [handleDismissNotification]);
|
||||
const handleUpdateDownloaded = (0, react_1.useCallback)((_event, info) => {
|
||||
if (notificationRef.current)
|
||||
return;
|
||||
const updateAvailableHtml = (0, html_1.htmlentities)((0, locale_1._)('A new update (%s) is available', info.version));
|
||||
const seeChangelogHtml = (0, html_1.htmlentities)((0, locale_1._)('See changelog'));
|
||||
const restartNowHtml = (0, html_1.htmlentities)((0, locale_1._)('Restart now'));
|
||||
const updateLaterHtml = (0, html_1.htmlentities)((0, locale_1._)('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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const notification = notyf.open({
|
||||
type: 'success',
|
||||
message: messageHtml,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
duration: 0,
|
||||
});
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
(0, react_1.useEffect)(() => {
|
||||
electron_1.ipcRenderer.on(AutoUpdaterService_1.AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
return () => {
|
||||
electron_1.ipcRenderer.removeListener(AutoUpdaterService_1.AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
};
|
||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
|
||||
return (React.createElement("div", { style: { display: 'none' } }));
|
||||
};
|
||||
exports.default = UpdateNotification;
|
||||
//# sourceMappingURL=UpdateNotification.js.map
|
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import NotyfContext from '../NotyfContext';
|
||||
import { UpdateInfo } from 'electron-updater';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
|
||||
import { NotyfNotification } from 'notyf';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
|
||||
interface UpdateNotificationProps {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
export enum UpdateNotificationEvents {
|
||||
ApplyUpdate = 'apply-update',
|
||||
Dismiss = 'dismiss-update-notification',
|
||||
}
|
||||
|
||||
const changelogLink = 'https://github.com/laurent22/joplin/releases';
|
||||
|
||||
window.openChangelogLink = () => {
|
||||
ipcRenderer.send('open-link', 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 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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const notification: NotyfNotification = notyf.open({
|
||||
type: 'success',
|
||||
message: messageHtml,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
};
|
||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: 'none' }}/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateNotification;
|