You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-02-10 08:14:27 +02:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c383996fa | ||
|
|
01b15d58dd | ||
|
|
fa07eb3db0 | ||
|
|
f439835281 | ||
|
|
740c87a817 | ||
|
|
8b3835eb04 | ||
|
|
a5318099c5 | ||
|
|
b39628a963 | ||
|
|
0386028803 | ||
|
|
ed242f736c | ||
|
|
8cd39e3b40 | ||
|
|
8d4632d9dd | ||
|
|
97a18f722d | ||
|
|
554e6efaab | ||
|
|
cd7af20bc1 | ||
|
|
4346616cae | ||
|
|
ef646adafa | ||
|
|
4ce47807b1 | ||
|
|
9b0bc4d600 | ||
|
|
91b8e4d34d | ||
|
|
9e9bd662dc | ||
|
|
defbbd5d72 | ||
|
|
b2c9dd40dc | ||
|
|
a9049111e4 | ||
|
|
55f642c625 | ||
|
|
dee9ec3495 | ||
|
|
97bf020150 | ||
|
|
2cb2680a5a | ||
|
|
242c6ec3b8 | ||
|
|
ed0b1ae390 | ||
|
|
de29e4ff92 | ||
|
|
9d96e31b83 | ||
|
|
aad460e9a1 | ||
|
|
00248a9177 | ||
|
|
af2926b634 | ||
|
|
916ed9bbfb | ||
|
|
b32015864e | ||
|
|
8939ef1c19 | ||
|
|
31bba39ae9 | ||
|
|
9dc49f0c24 | ||
|
|
02dfef11aa | ||
|
|
c278b45c78 | ||
|
|
0dafd21db0 | ||
|
|
490d35919c | ||
|
|
4c1ca5480d | ||
|
|
d414c6354a | ||
|
|
7651d8e3c4 | ||
|
|
d5c72c13cb | ||
|
|
4377634e7b | ||
|
|
69ec5c7f86 | ||
|
|
f02b0f48d8 | ||
|
|
4d77c1385f | ||
|
|
c83f9ddeac | ||
|
|
1b9c11df7b | ||
|
|
333a8723e8 | ||
|
|
e030c8271d | ||
|
|
560bc31445 | ||
|
|
c71aeb74b2 | ||
|
|
ffaf2acb66 | ||
|
|
f442f1fb23 | ||
|
|
81a1451820 | ||
|
|
b3a3d71461 | ||
|
|
1db38c3232 | ||
|
|
42e645eb70 | ||
|
|
3860f44d06 | ||
|
|
4df0f8668d | ||
|
|
306d0fddd8 | ||
|
|
56d12b28f2 | ||
|
|
6c5ea4872a | ||
|
|
9856e8ae93 | ||
|
|
5712da4c0f | ||
|
|
4f7ee56444 | ||
|
|
8e2b6ca296 | ||
|
|
0172bb0ad8 | ||
|
|
1d38e443ba | ||
|
|
5ad19b7261 | ||
|
|
70293478a2 | ||
|
|
3aaa20254f | ||
|
|
42c248f7ca | ||
|
|
ac1e94a8df | ||
|
|
daff4496cf | ||
|
|
1e00078228 | ||
|
|
03a1de9370 | ||
|
|
55ef256c65 | ||
|
|
6d115db16f | ||
|
|
5853031fde | ||
|
|
47db2ae962 | ||
|
|
b960a2a8b0 | ||
|
|
fcaa7d2a98 | ||
|
|
99284ae135 | ||
|
|
66ae58c81b | ||
|
|
484d6a866d | ||
|
|
b45fd09e38 | ||
|
|
903a369c13 | ||
|
|
1fb79315e4 | ||
|
|
4dc021b523 | ||
|
|
bbb4b46dd9 | ||
|
|
063dc46f50 | ||
|
|
aa400b52be | ||
|
|
be7de2f08a | ||
|
|
f8a129e4dc | ||
|
|
c5d9646908 | ||
|
|
876ec80911 | ||
|
|
4051f88ce7 | ||
|
|
f194c111e4 | ||
|
|
e386246bc9 | ||
|
|
292b269f1d | ||
|
|
b2fc43da2b | ||
|
|
4a23a1ed3e | ||
|
|
c8878a18bf | ||
|
|
340fba7af5 | ||
|
|
271c4f4a2a | ||
|
|
c9dba20f59 | ||
|
|
b474cc206a | ||
|
|
9d4df8cc6e | ||
|
|
a4ddfe1f58 | ||
|
|
7d15215e66 | ||
|
|
449555c8e9 |
@@ -92,6 +92,7 @@ readme/
|
||||
packages/react-native-vosk/lib/
|
||||
packages/lib/countable/Countable.js
|
||||
packages/onenote-converter/renderer/pkg/*
|
||||
packages/whisper-voice-typing/lib/
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
@@ -458,6 +459,7 @@ packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WebDavOidcLoginScreen.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
|
||||
@@ -468,6 +470,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||
@@ -511,6 +515,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderR
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/utils/canUseNativeUndo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||
@@ -691,6 +696,7 @@ packages/app-mobile/components/FeedbackBanner.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/KeyboardAvoidingView.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NestableFlatList.js
|
||||
@@ -869,8 +875,10 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/screens/webdav-oidc-login.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
@@ -969,6 +977,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initReact.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -1045,6 +1054,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
@@ -1061,6 +1072,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1101,6 +1114,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1230,6 +1244,7 @@ packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.test.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/OidcApi.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
packages/lib/PoorManIntervals.js
|
||||
@@ -1239,13 +1254,17 @@ packages/lib/SyncTargetFilesystem.js
|
||||
packages/lib/SyncTargetJoplinCloud.js
|
||||
packages/lib/SyncTargetJoplinServer.js
|
||||
packages/lib/SyncTargetJoplinServerSAML.js
|
||||
packages/lib/SyncTargetNextcloud.js
|
||||
packages/lib/SyncTargetNone.js
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/SyncTargetWebDAV.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WebDavApi.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/base-oauth-node-utils.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
@@ -1395,6 +1414,8 @@ packages/lib/models/utils/userData.test.js
|
||||
packages/lib/models/utils/userData.js
|
||||
packages/lib/net-utils.js
|
||||
packages/lib/ntp.js
|
||||
packages/lib/oidc-api-node-utils.js
|
||||
packages/lib/onedrive-api-node-utils.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
@@ -1704,6 +1725,7 @@ packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
packages/lib/testing/test-utils-synchronizer.js
|
||||
packages/lib/testing/test-utils.js
|
||||
packages/lib/testing/waitFor.js
|
||||
packages/lib/theme.js
|
||||
packages/lib/themes/aritimDark.js
|
||||
packages/lib/themes/dark.js
|
||||
@@ -1808,6 +1830,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.test.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
packages/renderer/MdToHtml/rules/html_image.js
|
||||
packages/renderer/MdToHtml/rules/image.js
|
||||
@@ -1841,22 +1865,29 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/extractResourceIds.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/hangingIndent.js
|
||||
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/randomId.test.js
|
||||
packages/tools/fuzzer/utils/randomId.js
|
||||
packages/tools/fuzzer/utils/randomString.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
@@ -1928,4 +1959,6 @@ packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/processTranslations.js
|
||||
packages/tools/website/utils/render.js
|
||||
packages/tools/website/utils/types.js
|
||||
packages/whisper-voice-typing/src/index.js
|
||||
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
||||
@@ -314,7 +314,7 @@ module.exports = {
|
||||
selector: 'interface',
|
||||
format: null,
|
||||
'filter': {
|
||||
'regex': '^(RSA|RSAKeyPair|iOS.*)$',
|
||||
'regex': '^(RSA|RSAKeyPair|iOS.*|OAuth.*)$',
|
||||
'match': true,
|
||||
},
|
||||
},
|
||||
|
||||
34
.gitignore
vendored
34
.gitignore
vendored
@@ -432,6 +432,7 @@ packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WebDavOidcLoginScreen.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
|
||||
@@ -442,6 +443,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||
@@ -485,6 +488,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderR
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/utils/canUseNativeUndo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||
@@ -665,6 +669,7 @@ packages/app-mobile/components/FeedbackBanner.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/KeyboardAvoidingView.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NestableFlatList.js
|
||||
@@ -843,8 +848,10 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/screens/webdav-oidc-login.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
|
||||
@@ -943,6 +950,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initReact.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -1019,6 +1027,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
@@ -1035,6 +1045,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1075,6 +1087,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1204,6 +1217,7 @@ packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.test.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/OidcApi.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
packages/lib/PoorManIntervals.js
|
||||
@@ -1213,13 +1227,17 @@ packages/lib/SyncTargetFilesystem.js
|
||||
packages/lib/SyncTargetJoplinCloud.js
|
||||
packages/lib/SyncTargetJoplinServer.js
|
||||
packages/lib/SyncTargetJoplinServerSAML.js
|
||||
packages/lib/SyncTargetNextcloud.js
|
||||
packages/lib/SyncTargetNone.js
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/SyncTargetWebDAV.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WebDavApi.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/base-oauth-node-utils.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
@@ -1369,6 +1387,8 @@ packages/lib/models/utils/userData.test.js
|
||||
packages/lib/models/utils/userData.js
|
||||
packages/lib/net-utils.js
|
||||
packages/lib/ntp.js
|
||||
packages/lib/oidc-api-node-utils.js
|
||||
packages/lib/onedrive-api-node-utils.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
@@ -1678,6 +1698,7 @@ packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
packages/lib/testing/test-utils-synchronizer.js
|
||||
packages/lib/testing/test-utils.js
|
||||
packages/lib/testing/waitFor.js
|
||||
packages/lib/theme.js
|
||||
packages/lib/themes/aritimDark.js
|
||||
packages/lib/themes/dark.js
|
||||
@@ -1782,6 +1803,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.test.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
packages/renderer/MdToHtml/rules/html_image.js
|
||||
packages/renderer/MdToHtml/rules/image.js
|
||||
@@ -1815,22 +1838,29 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/extractResourceIds.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/hangingIndent.js
|
||||
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/randomId.test.js
|
||||
packages/tools/fuzzer/utils/randomId.js
|
||||
packages/tools/fuzzer/utils/randomString.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
@@ -1902,5 +1932,7 @@ packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/processTranslations.js
|
||||
packages/tools/website/utils/render.js
|
||||
packages/tools/website/utils/types.js
|
||||
packages/whisper-voice-typing/src/index.js
|
||||
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"exceptions": [
|
||||
"@joplin/editor",
|
||||
"@joplin/fork-htmlparser2",
|
||||
"@joplin/whisper-voice-typing",
|
||||
"@joplin/fork-sax",
|
||||
"@joplin/fork-uslug",
|
||||
"@joplin/htmlpack",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index a16b4ad6d1871cf5cf73ef7ebeaf8bd4d662b134..9871afb5fbf8e687370e08f54d884ecd7dde7e7c 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index d42bd23123644cc324051e9c7ec4635de286315a..640996df60fe7769f69b30b35f771eb9cf0b75d4 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 170ec0ff9befe0f9155aaf5e1b84133cfd87be99..e6a0ab4a019ee67c5af7761ae8bb35f18b05c590 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 3b22f9de66795ee01dbaa29655727ee7ddba3cc8..325daa88d33f066b3826e5031ce281793710af2d 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -6,7 +6,7 @@ Only the latest version is supported with security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/AdresseSupport.png) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
|
||||
Please report vulnerabilities [through private vulnerability reporting](https://github.com/laurent22/joplin/security/advisories/new) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
|
||||
|
||||
For general opinions on what makes an app more or less secure, please use the forum.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"/packages/app-desktop/build/",
|
||||
"/packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts",
|
||||
"/packages/app-desktop/vendor/",
|
||||
"/packages/app-mobile/android/vendor/",
|
||||
"/packages/whisper-voice-typing/vendor/",
|
||||
"/packages/app-mobile/ios/Pods/",
|
||||
"/packages/app-mobile/lib/rnInjectedJs",
|
||||
"/packages/app-mobile/pluginAssets",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "24.5.0",
|
||||
"nodejs": "24.8.0",
|
||||
"pkg-config": "latest",
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
|
||||
23
docker-compose-oidc.yml
Normal file
23
docker-compose-oidc.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
ocis:
|
||||
image: owncloud/ocis:latest
|
||||
container_name: ocis
|
||||
entrypoint: /bin/sh
|
||||
command: ["-c", "ocis init --insecure true || true; ocis server"]
|
||||
environment:
|
||||
OCIS_URL: https://localhost:9200
|
||||
OCIS_INSECURE: "true"
|
||||
PROXY_ENABLE_BASIC_AUTH: "false"
|
||||
IDM_ADMIN_PASSWORD: admin
|
||||
OCIS_LOG_LEVEL: warn
|
||||
# Allow Joplin's redirect URIs
|
||||
IDP_INSECURE: "true"
|
||||
IDP_IDENTIFIER_REGISTRATION_CONF: /etc/ocis/clients.yaml
|
||||
ports:
|
||||
- "9200:9200"
|
||||
volumes:
|
||||
- ocis_data:/var/lib/ocis
|
||||
- ./ocis-clients.yaml:/etc/ocis/clients.yaml:ro
|
||||
|
||||
volumes:
|
||||
ocis_data:
|
||||
12
ocis-clients.yaml
Normal file
12
ocis-clients.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
clients:
|
||||
- id: joplin
|
||||
name: Joplin
|
||||
application_type: native
|
||||
redirect_uris:
|
||||
- http://localhost:9968
|
||||
- http://localhost:8968
|
||||
- http://localhost:8868
|
||||
- http://127.0.0.1:9968
|
||||
- http://127.0.0.1:8968
|
||||
- http://127.0.0.1:8868
|
||||
- joplin://oidc-callback
|
||||
@@ -86,9 +86,9 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "16.1.6",
|
||||
"lint-staged": "16.2.6",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"npm-package-json-lint": "9.0.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import mockShareService from '@joplin/lib/testing/share/mockShareService';
|
||||
import { createFolderTree, setupDatabaseAndSynchronizer, switchClient, waitFor } from '@joplin/lib/testing/test-utils';
|
||||
import { createFolderTree, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import waitFor from '@joplin/lib/testing/waitFor';
|
||||
import { setupApplication, setupCommandForTesting } from './utils/testUtils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { masterKeysWithoutPassword } from '@joplin/lib/services/e2ee/utils';
|
||||
import { appTypeToLockType } from '@joplin/lib/services/synchronizer/LockHandler';
|
||||
const BaseCommand = require('./base-command').default;
|
||||
import app from './app';
|
||||
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
|
||||
import OneDriveApiNodeUtils from '@joplin/lib/onedrive-api-node-utils';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const md5 = require('md5');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import mockShareService from '@joplin/lib/testing/share/mockShareService';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, waitFor } from '@joplin/lib/testing/test-utils';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import waitFor from '@joplin/lib/testing/waitFor';
|
||||
import { setupApplication, setupCommandForTesting } from './utils/testUtils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.4",
|
||||
"sharp": "0.34.5",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
|
||||
@@ -12,6 +12,7 @@ function newTestMdToHtml(options: any = null) {
|
||||
ResourceModel: {
|
||||
isResourceUrl: isResourceUrl,
|
||||
urlToId: resourceUrlToId,
|
||||
fullPath: () => '/some/path/here',
|
||||
},
|
||||
fsDriver: shim.fsDriver(),
|
||||
...options,
|
||||
@@ -56,6 +57,21 @@ describe('MdToHtml', () => {
|
||||
mdToHtmlOptions.mapsToLine = true;
|
||||
} else if (mdFilename.startsWith('resource_')) {
|
||||
mdToHtmlOptions.resources = {};
|
||||
} else if (mdFilename.startsWith('pdf_')) {
|
||||
mdToHtmlOptions.resources = {
|
||||
'00000000000000000000000000000001': {
|
||||
item: { mime: 'application/pdf' },
|
||||
localState: { },
|
||||
},
|
||||
};
|
||||
mdToHtmlOptions.pdfViewerEnabled = true;
|
||||
} else if (mdFilename.startsWith('video_')) {
|
||||
mdToHtmlOptions.resources = {
|
||||
'00000000000000000000000000000001': {
|
||||
item: { mime: 'video/mp4' },
|
||||
localState: { },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
||||
@@ -86,7 +102,7 @@ describe('MdToHtml', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(msg.join('\n'));
|
||||
|
||||
expect(false).toBe(true);
|
||||
expect(actualHtml).toBe(expectedHtml);
|
||||
// return;
|
||||
} else {
|
||||
expect(true).toBe(true);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="joplin-editable">
|
||||
<!-- Regression test: Historically, text nodes before the first "joplin-source" block caused
|
||||
conversion to fail. -->
|
||||
A text node!
|
||||
<pre class="joplin-source" data-joplin-language="test" data-joplin-source-open="``` " data-joplin-source-close=" ```">
|
||||
Test!
|
||||
</pre>
|
||||
<div class="joplin-rendered">
|
||||
<p>Test content</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
```
|
||||
Test!
|
||||
|
||||
```
|
||||
4
packages/app-cli/tests/md_to_html/pdf_embed.html
Normal file
4
packages/app-cli/tests/md_to_html/pdf_embed.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<p>Embed without starting page:</p>
|
||||
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='application/pdf' href='#' onclick='postMessage("joplin://00000000000000000000000000000001", { resourceId: "00000000000000000000000000000001" }); return false;'><span class="resource-icon fa-file-pdf"></span>pdf</a><object data="file:///some/path/here" class="media-player media-pdf" type="application/pdf"></object></p>
|
||||
<p>Embed with starting page:</p>
|
||||
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='application/pdf' href='#' onclick='postMessage("joplin://00000000000000000000000000000001#page=1", { resourceId: "00000000000000000000000000000001" }); return false;'><span class="resource-icon fa-file-pdf"></span>pdf</a><object data="file:///some/path/here#page=1" class="media-player media-pdf" type="application/pdf"></object></p>
|
||||
8
packages/app-cli/tests/md_to_html/pdf_embed.md
Normal file
8
packages/app-cli/tests/md_to_html/pdf_embed.md
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
Embed without starting page:
|
||||
|
||||
[pdf](:/00000000000000000000000000000001)
|
||||
|
||||
Embed with starting page:
|
||||
|
||||
[pdf](:/00000000000000000000000000000001#page=1)
|
||||
10
packages/app-cli/tests/md_to_html/video_embed.html
Normal file
10
packages/app-cli/tests/md_to_html/video_embed.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='video/mp4' href='#' onclick='postMessage("joplin://00000000000000000000000000000001#t=1,2", { resourceId: "00000000000000000000000000000001" }); return false;'><span class="resource-icon fa-file-video"></span>video, with start/end time</a>
|
||||
<video class="media-player media-video" controls>
|
||||
<source src="file:///some/path/here#t=1,2" type="video/mp4">
|
||||
</video>
|
||||
</p>
|
||||
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='video/mp4' href='#' onclick='postMessage("joplin://00000000000000000000000000000001", { resourceId: "00000000000000000000000000000001" }); return false;'><span class="resource-icon fa-file-video"></span>video, without start/end time</a>
|
||||
<video class="media-player media-video" controls>
|
||||
<source src="file:///some/path/here" type="video/mp4">
|
||||
</video>
|
||||
</p>
|
||||
4
packages/app-cli/tests/md_to_html/video_embed.md
Normal file
4
packages/app-cli/tests/md_to_html/video_embed.md
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
[video, with start/end time](:/00000000000000000000000000000001#t=1,2)
|
||||
|
||||
[video, without start/end time](:/00000000000000000000000000000001)
|
||||
BIN
packages/app-cli/tests/support/onenote/test.onepkg
Normal file
BIN
packages/app-cli/tests/support/onenote/test.onepkg
Normal file
Binary file not shown.
@@ -95,6 +95,9 @@ export default class InteropServiceHelper {
|
||||
// Allows users to override the CSS page size.
|
||||
// See https://github.com/laurent22/joplin/issues/13096
|
||||
preferCSSPageSize: true,
|
||||
|
||||
// Include accessibility information in the output:
|
||||
generateTaggedPDF: true,
|
||||
});
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -76,6 +76,19 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if WebDAV with OIDC authentication needs login
|
||||
if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('webdav') &&
|
||||
this.state.settings['sync.6.authType'] === 'oidc') {
|
||||
const isAuthenticated = await reg.syncTarget().isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
return this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'WebDavOidcLogin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
@@ -115,6 +128,13 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
} else if (key === 'sync.6.oidcLogin') {
|
||||
// Save current settings before navigating to login
|
||||
await shared.saveSettings(this);
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'WebDavOidcLogin',
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unhandled key: ${key}`);
|
||||
}
|
||||
|
||||
@@ -693,17 +693,8 @@ function useMenu(props: Props) {
|
||||
menuItemDic.pasteAsText,
|
||||
menuItemDic.textSelectAll,
|
||||
separator(),
|
||||
// Using the generic "undo"/"redo" roles mean the menu
|
||||
// item will work in every text fields, whether it's the
|
||||
// editor or a regular text field.
|
||||
{
|
||||
role: 'undo',
|
||||
label: _('Undo'),
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
label: _('Redo'),
|
||||
},
|
||||
menuItemDic.globalUndo,
|
||||
menuItemDic.globalRedo,
|
||||
separator(),
|
||||
menuItemDic.textBold,
|
||||
menuItemDic.textItalic,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { ContextMenuParams, Event } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { useEffect, RefObject, useContext } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { EditContextMenuFilterObject, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
@@ -11,6 +11,7 @@ import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -29,6 +30,7 @@ interface ContextMenuProps {
|
||||
|
||||
const useContextMenu = (props: ContextMenuProps) => {
|
||||
const editorRef = props.editorRef;
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
// The below code adds support for spellchecking when it is enabled
|
||||
// It might be buggy, refer to the below issue
|
||||
@@ -156,7 +158,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
|
||||
// Prepend the event listener so that it gets called before
|
||||
// the listener that shows the default menu.
|
||||
const targetWindow = bridge().activeWindow();
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
targetWindow.webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
return () => {
|
||||
@@ -167,6 +169,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef, props.containerRef,
|
||||
props.editorCutText, props.editorCopyText, props.editorPaste,
|
||||
windowId,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -294,6 +294,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
window.requestAnimationFrame(() => editor.undoManager.add());
|
||||
},
|
||||
pasteAsText: () => editor.fire(TinyMceEditorEvents.PasteAsText),
|
||||
|
||||
'editor.undo': () => {
|
||||
editor.undoManager.undo();
|
||||
},
|
||||
'editor.redo': () => {
|
||||
editor.undoManager.redo();
|
||||
},
|
||||
};
|
||||
|
||||
if (additionalCommands[cmd.name]) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
|
||||
import { menuItems } from '../../../utils/contextMenu';
|
||||
@@ -18,6 +18,7 @@ import { Dispatch } from 'redux';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import type { MenuItem as MenuItemType } from 'electron';
|
||||
import isItemId from '@joplin/lib/models/utils/isItemId';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -30,11 +31,12 @@ interface ContextMenuActionOptions {
|
||||
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
|
||||
|
||||
export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatch, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler, editDialog: EditDialogControl) {
|
||||
const windowId = useContext(WindowIdContext);
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const contextMenuItems = menuItems(dispatch);
|
||||
const targetWindow = bridge().activeWindow();
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
|
||||
const makeMainMenuItems = (element: Element) => {
|
||||
let itemType: ContextMenuItemType = ContextMenuItemType.None;
|
||||
@@ -175,5 +177,5 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
targetWindow.webContents.off('context-menu', onElectronContextMenu);
|
||||
}
|
||||
};
|
||||
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
|
||||
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog, windowId]);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ function newBlockSource(language = '', content = '', previousSource: SourceInfo
|
||||
} else {
|
||||
fence = '$$';
|
||||
}
|
||||
} else if (language === 'frontmatter') {
|
||||
// Frontmatter uses --- delimiters instead of code fences
|
||||
return {
|
||||
openCharacters: '---\n',
|
||||
closeCharacters: '\n---\n',
|
||||
content: content,
|
||||
node: null,
|
||||
language: language,
|
||||
};
|
||||
}
|
||||
|
||||
const fenceLanguage = language === 'katex' ? '' : language;
|
||||
|
||||
@@ -58,6 +58,17 @@ const usePluginMessageResponder = (webviewRef: RefObject<HTMLIFrameElement>) =>
|
||||
}, [webviewRef, windowId]);
|
||||
};
|
||||
|
||||
const useAllowAttribute = () => {
|
||||
// Specifies what content in the note viewer can do. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/allow
|
||||
// allow=fullscreen: Required to allow the user to fullscreen videos.
|
||||
return [
|
||||
'clipboard-write', 'fullscreen', 'autoplay', 'local-fonts', 'encrypted-media',
|
||||
].map(
|
||||
attr => `${attr} joplin-content://note-viewer/`,
|
||||
).join('; ');
|
||||
};
|
||||
|
||||
const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerControl>) => {
|
||||
const [webview, setWebview] = useState<HTMLIFrameElement|null>(null);
|
||||
const webviewRef = useRef<HTMLIFrameElement|null>(null);
|
||||
@@ -233,14 +244,13 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
|
||||
return { border: 'none', ...props.viewerStyle };
|
||||
}, [props.viewerStyle]);
|
||||
|
||||
// allow=fullscreen: Required to allow the user to fullscreen videos.
|
||||
const allow = useAllowAttribute();
|
||||
return (
|
||||
<iframe
|
||||
className="noteTextViewer"
|
||||
ref={setWebview}
|
||||
style={viewerStyle}
|
||||
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
|
||||
allowFullScreen={true}
|
||||
allow={allow}
|
||||
aria-label={_('Note viewer')}
|
||||
src={`joplin-content://note-viewer/${toForwardSlashes(getAssetPath('gui/note-viewer/index.html'))}`}
|
||||
></iframe>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../services/bridge';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
|
||||
import OneDriveApiNodeUtils from '@joplin/lib/onedrive-api-node-utils';
|
||||
|
||||
interface Props {
|
||||
themeId: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import ConfigScreen from './ConfigScreen/ConfigScreen';
|
||||
import StatusScreen from './StatusScreen/StatusScreen';
|
||||
import OneDriveLoginScreen from './OneDriveLoginScreen';
|
||||
import DropboxLoginScreen from './DropboxLoginScreen';
|
||||
import WebDavOidcLoginScreen from './WebDavOidcLoginScreen';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import MenuBar from './MenuBar';
|
||||
@@ -163,6 +164,7 @@ class RootComponent extends React.Component<Props, any> {
|
||||
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
|
||||
JoplinCloudLogin: { screen: JoplinCloudLoginScreen, title: () => _('Joplin Cloud Login') },
|
||||
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()), title: () => _('Joplin Server Login') },
|
||||
WebDavOidcLogin: { screen: WebDavOidcLoginScreen, title: () => _('WebDAV OIDC Login') },
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
||||
|
||||
33
packages/app-desktop/gui/WebDavOidcLoginScreen.scss
Normal file
33
packages/app-desktop/gui/WebDavOidcLoginScreen.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
.webdav-oidc-login-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--joplin-background-color);
|
||||
|
||||
> .content {
|
||||
padding: var(--joplin-config-screen-padding);
|
||||
flex: 1;
|
||||
color: var(--joplin-color);
|
||||
|
||||
> .title {
|
||||
font-size: var(--joplin-h1-font-size);
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
> .logentry {
|
||||
font-size: var(--joplin-font-size);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .loglink {
|
||||
color: var(--joplin-url-color);
|
||||
font-size: var(--joplin-font-size);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
packages/app-desktop/gui/WebDavOidcLoginScreen.tsx
Normal file
111
packages/app-desktop/gui/WebDavOidcLoginScreen.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../services/bridge';
|
||||
import { OidcApiNodeUtils } from '@joplin/lib/oidc-api-node-utils';
|
||||
import OidcApi from '@joplin/lib/OidcApi';
|
||||
|
||||
interface LogEntry {
|
||||
key: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const WebDavOidcLoginScreen: React.FC = () => {
|
||||
const [authLog, setAuthLog] = useState<LogEntry[]>([]);
|
||||
const oidcApiUtilsRef = useRef<OidcApiNodeUtils | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const log = useCallback((s: string) => {
|
||||
setAuthLog(prevLog => [
|
||||
...prevLog,
|
||||
{ key: `${Date.now()}-${Math.random()}`, text: s },
|
||||
]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const performAuth = async () => {
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
|
||||
const oidcApi = new OidcApi({
|
||||
issuerUrl: Setting.value('sync.6.oidcIssuerUrl'),
|
||||
clientId: Setting.value('sync.6.oidcClientId'),
|
||||
clientSecret: Setting.value('sync.6.oidcClientSecret'),
|
||||
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
|
||||
});
|
||||
|
||||
oidcApiUtilsRef.current = new OidcApiNodeUtils(oidcApi);
|
||||
|
||||
try {
|
||||
const auth = await oidcApiUtilsRef.current.oauthDance({
|
||||
log: (s: string) => log(s),
|
||||
});
|
||||
|
||||
Setting.setValue(`sync.${syncTargetId}.oidcAuth`, auth ? JSON.stringify(auth) : '');
|
||||
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
if (syncTarget.api && syncTarget.api()) {
|
||||
syncTarget.api().setAuth(auth);
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
log(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
} else {
|
||||
log(_('Authentication successful! You can now close this screen.'));
|
||||
void reg.scheduleSync(0);
|
||||
}
|
||||
} catch (error) {
|
||||
log(_('Authentication failed: %s', (error as Error).message));
|
||||
}
|
||||
};
|
||||
|
||||
void performAuth();
|
||||
|
||||
return () => {
|
||||
if (oidcApiUtilsRef.current) {
|
||||
oidcApiUtilsRef.current.cancelOAuthDance();
|
||||
}
|
||||
};
|
||||
}, [log]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
}, [dispatch]);
|
||||
|
||||
const handleLinkClick = useCallback((url: string) => {
|
||||
void bridge().openExternal(url);
|
||||
}, []);
|
||||
|
||||
const renderLogEntries = () => {
|
||||
return authLog.map(entry => {
|
||||
if (entry.text.indexOf('http:') === 0 || entry.text.indexOf('https://') === 0) {
|
||||
return (
|
||||
<a
|
||||
key={entry.key}
|
||||
className="loglink"
|
||||
href="#"
|
||||
onClick={() => handleLinkClick(entry.text)}
|
||||
>
|
||||
{entry.text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <p key={entry.key} className="logentry">{entry.text}</p>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="webdav-oidc-login-screen">
|
||||
<div className="content">
|
||||
<h1 className="title">{_('WebDAV OIDC Authentication')}</h1>
|
||||
{renderLogEntries()}
|
||||
</div>
|
||||
<ButtonBar onCancelClick={handleCancelClick} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebDavOidcLoginScreen;
|
||||
@@ -0,0 +1,24 @@
|
||||
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { WindowControl } from '../utils/useWindowControl';
|
||||
import bridge from '../../../services/bridge';
|
||||
import canUseNativeUndo from './utils/canUseNativeUndo';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'globalRedo',
|
||||
label: () => _('Redo'),
|
||||
};
|
||||
|
||||
export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
if (canUseNativeUndo(control)) {
|
||||
bridge().activeWindow().webContents.redo();
|
||||
} else {
|
||||
await CommandService.instance().execute('editor.redo');
|
||||
}
|
||||
},
|
||||
|
||||
enabledCondition: '',
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { WindowControl } from '../utils/useWindowControl';
|
||||
import bridge from '../../../services/bridge';
|
||||
import canUseNativeUndo from './utils/canUseNativeUndo';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'globalUndo',
|
||||
label: () => _('Undo'),
|
||||
};
|
||||
|
||||
export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
// As of January 2026, webContents.undo() doesn't work properly in more complex
|
||||
// edit controls (e.g. CodeMirror or TinyMCE). Only use it when a more simple input
|
||||
// has focus:
|
||||
if (canUseNativeUndo(control)) {
|
||||
bridge().activeWindow().webContents.undo();
|
||||
} else {
|
||||
await CommandService.instance().execute('editor.undo');
|
||||
}
|
||||
},
|
||||
|
||||
enabledCondition: '',
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,8 @@ import * as deleteFolder from './deleteFolder';
|
||||
import * as duplicateNote from './duplicateNote';
|
||||
import * as editAlarm from './editAlarm';
|
||||
import * as exportPdf from './exportPdf';
|
||||
import * as globalRedo from './globalRedo';
|
||||
import * as globalUndo from './globalUndo';
|
||||
import * as gotoAnything from './gotoAnything';
|
||||
import * as hideModalMessage from './hideModalMessage';
|
||||
import * as importFrom from './importFrom';
|
||||
@@ -54,6 +56,8 @@ const index: any[] = [
|
||||
duplicateNote,
|
||||
editAlarm,
|
||||
exportPdf,
|
||||
globalRedo,
|
||||
globalUndo,
|
||||
gotoAnything,
|
||||
hideModalMessage,
|
||||
importFrom,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WindowControl } from '../../utils/useWindowControl';
|
||||
|
||||
// CodeMirror and TinyMCE both have trouble with native Electron
|
||||
// undo/redo.
|
||||
// See https://github.com/laurent22/joplin/issues/14216
|
||||
const canUseNativeUndo = (control: WindowControl) => {
|
||||
const dom = control.getFocusedDocument();
|
||||
return !dom.activeElement.closest('.CodeMirror, div.joplin-tinymce');
|
||||
};
|
||||
|
||||
export default canUseNativeUndo;
|
||||
@@ -21,7 +21,7 @@ const useWindowCommands = ({ documentRef, customCss, plugins, editorNoteStatuses
|
||||
editorNoteStatuses: editorNoteStatuses,
|
||||
plugins: plugins,
|
||||
});
|
||||
const windowControl = useWindowControl(setDialogState, onPrintCallback);
|
||||
const windowControl = useWindowControl(setDialogState, onPrintCallback, documentRef);
|
||||
|
||||
// This effect needs to run as soon as possible. Certain components may fail to load if window
|
||||
// commands are not registered on their first render.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { RefObject, useMemo, useRef } from 'react';
|
||||
import { DialogState } from '../types';
|
||||
import { PrintCallback } from './usePrintToCallback';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
@@ -23,10 +23,11 @@ export interface WindowControl {
|
||||
showPrompt: <T>(options: PromptOptions<T>)=> Promise<T>;
|
||||
printTo: PrintCallback;
|
||||
announcePanelVisibility(panelName: string, visible: boolean): void;
|
||||
getFocusedDocument(): Document;
|
||||
}
|
||||
|
||||
export type OnSetDialogState = React.Dispatch<React.SetStateAction<DialogState>>;
|
||||
const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback) => {
|
||||
const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback, windowDomRef: RefObject<Document>) => {
|
||||
// Use refs to avoid reloading the output where possible -- reloading the window control
|
||||
// may mean reloading all main window commands.
|
||||
const onPrintRef = useRef(onPrint);
|
||||
@@ -67,9 +68,12 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
|
||||
});
|
||||
});
|
||||
},
|
||||
getFocusedDocument: () => {
|
||||
return windowDomRef.current;
|
||||
},
|
||||
};
|
||||
return control;
|
||||
}, [setDialogState]);
|
||||
}, [setDialogState, windowDomRef]);
|
||||
};
|
||||
|
||||
export default useWindowControl;
|
||||
|
||||
@@ -50,13 +50,16 @@ export default function() {
|
||||
'editor.duplicateLine',
|
||||
'openSecondaryAppInstance',
|
||||
'openPrimaryAppInstance',
|
||||
// We cannot put the undo/redo commands in the menu because they are
|
||||
// editor-specific commands. If we put them there it will break the
|
||||
// undo/redo in regular text fields.
|
||||
// https://github.com/laurent22/joplin/issues/6214
|
||||
|
||||
// 'editor.undo',
|
||||
// 'editor.redo',
|
||||
// We cannot put the editor.undo/editor.redo commands in the menu because they are
|
||||
// editor-specific commands. If we put them there it will break the undo/redo in
|
||||
// regular text fields (https://github.com/laurent22/joplin/issues/6214).
|
||||
// However, the native Electron undo/redo doesn't work well in TinyMCE/CodeMirror.
|
||||
// As a workaround, use these commands that switch between editor.undo and native Electron
|
||||
// undo/redo depending on the type of selected editor:
|
||||
'globalUndo',
|
||||
'globalRedo',
|
||||
|
||||
'editor.indentLess',
|
||||
'editor.indentMore',
|
||||
'editor.toggleComment',
|
||||
|
||||
@@ -381,5 +381,24 @@ test.describe('markdownEditor', () => {
|
||||
await goToAnything.runCommand(electronApp, 'textPaste');
|
||||
await noteEditor.expectToHaveText(/^Test \(new content!\)[\n]+/);
|
||||
});
|
||||
|
||||
test('the undo and redo menu items should work', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.createNewNote('Test undo/redo');
|
||||
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
|
||||
await mainWindow.keyboard.type('A');
|
||||
await noteEditor.expectToHaveText('A');
|
||||
|
||||
await activateMainMenuItem(electronApp, 'Undo');
|
||||
await noteEditor.expectToHaveText('\n');
|
||||
|
||||
await activateMainMenuItem(electronApp, 'Redo');
|
||||
await noteEditor.expectToHaveText('A');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const waitFor = async (condition) => {
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
for (let i = 0; i < 500; i++) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,9 @@ module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
testEnvironmentOptions: {
|
||||
customExportConditions: ['node', 'require'],
|
||||
},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.6.1",
|
||||
"version": "3.6.2",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -169,7 +169,7 @@
|
||||
"debounce": "1.2.1",
|
||||
"electron": "39.2.3",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-updater": "6.6.8",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
@@ -185,7 +185,7 @@
|
||||
"md5": "2.3.0",
|
||||
"moment": "2.30.1",
|
||||
"mustache": "4.2.0",
|
||||
"nan": "2.23.0",
|
||||
"nan": "2.23.1",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-rsa": "1.1.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@use 'gui/NoteList/style.scss' as note-list;
|
||||
@use 'gui/SsoLoginScreen/SsoLoginScreen.scss' as sso-login-screen;
|
||||
@use 'gui/JoplinCloudLoginScreen.scss' as joplin-cloud-login-screen;
|
||||
@use 'gui/WebDavOidcLoginScreen.scss' as webdav-oidc-login-screen;
|
||||
@use 'gui/NoteListHeader/style.scss' as note-list-header;
|
||||
@use 'gui/UpdateNotification/style.scss' as update-notification;
|
||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||
|
||||
90
packages/app-desktop/tools/notarizeFile.js
Normal file
90
packages/app-desktop/tools/notarizeFile.js
Normal file
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.default = notarizeFile;
|
||||
const fs_1 = require('fs');
|
||||
const notarize_1 = require('@electron/notarize');
|
||||
const execCommand = require('./execCommand');
|
||||
const child_process_1 = require('child_process');
|
||||
const util_1 = require('util');
|
||||
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
||||
// Same appId in electron-builder.
|
||||
const appId = 'net.cozic.joplin-desktop';
|
||||
function isDesktopAppTag(tagName) {
|
||||
if (!tagName) { return false; }
|
||||
return tagName[0] === 'v';
|
||||
}
|
||||
async function notarizeFile(filePath) {
|
||||
if (process.platform !== 'darwin') { return; }
|
||||
console.info(`Checking if notarization should be done on: ${filePath}`);
|
||||
if (!process.env.IS_CONTINUOUS_INTEGRATION || !isDesktopAppTag(process.env.GIT_TAG_NAME)) {
|
||||
console.info(`Either not running in CI or not processing a desktop app tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
|
||||
return;
|
||||
}
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
|
||||
console.warn('Environment variables APPLE_ID and APPLE_ID_PASSWORD not found - notarization will NOT be done.');
|
||||
return;
|
||||
}
|
||||
if (!(0, fs_1.existsSync)(filePath)) {
|
||||
throw new Error(`Cannot find file at: ${filePath}`);
|
||||
}
|
||||
// Every x seconds we print something to stdout, otherwise CI may timeout
|
||||
// the task after 10 minutes, and Apple notarization can take more time.
|
||||
const waitingIntervalId = setInterval(() => {
|
||||
console.info('.');
|
||||
}, 60000);
|
||||
const isPkg = filePath.endsWith('.pkg');
|
||||
console.info(`Notarizing ${filePath}`);
|
||||
try {
|
||||
if (isPkg) {
|
||||
await execAsync(`xcrun notarytool submit "${filePath}" ` +
|
||||
`--apple-id "${process.env.APPLE_ID}" ` +
|
||||
`--password "${process.env.APPLE_ID_PASSWORD}" ` +
|
||||
`--team-id "${process.env.APPLE_ASC_PROVIDER}" ` +
|
||||
'--wait', { maxBuffer: 1024 * 1024 });
|
||||
} else {
|
||||
await (0, notarize_1.notarize)({
|
||||
appBundleId: appId,
|
||||
appPath: filePath,
|
||||
// Apple Developer email address
|
||||
appleId: process.env.APPLE_ID,
|
||||
// App-specific password: https://support.apple.com/en-us/HT204397
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD,
|
||||
// When Apple ID is attached to multiple providers (eg if the
|
||||
// account has been used to build multiple apps for different
|
||||
// companies), in that case the provider "Team Short Name" (also
|
||||
// known as "ProviderShortname") must be provided.
|
||||
//
|
||||
// Use this to get it:
|
||||
//
|
||||
// xcrun altool --list-providers -u APPLE_ID -p APPLE_ID_PASSWORD
|
||||
// ascProvider: process.env.APPLE_ASC_PROVIDER,
|
||||
// In our case, the team ID is the same as the legacy ASC_PROVIDER
|
||||
teamId: process.env.APPLE_ASC_PROVIDER,
|
||||
tool: 'notarytool',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
clearInterval(waitingIntervalId);
|
||||
// It appears that electron-notarize doesn't staple the app, but without
|
||||
// this we were still getting the malware warning when launching the app.
|
||||
// Stapling the app means attaching the notarization ticket to it, so that
|
||||
// if the user is offline, macOS can still check if the app was notarized.
|
||||
// So it seems to be more or less optional, but at least in our case it
|
||||
// wasn't.
|
||||
console.info('Stapling notarization ticket to the file...');
|
||||
const staplerCmd = `xcrun stapler staple "${filePath}"`;
|
||||
console.info(`> ${staplerCmd}`);
|
||||
console.info(await execCommand(staplerCmd));
|
||||
console.info(`Validating stapled file: ${filePath}`);
|
||||
try {
|
||||
await execAsync(`spctl -a -vv -t install "${filePath}"`);
|
||||
} catch (error) {
|
||||
console.error(`Failed validating stapled file: ${filePath}:`, error);
|
||||
}
|
||||
console.info(`Done notarizing ${filePath}`);
|
||||
}
|
||||
// # sourceMappingURL=notarizeFile.js.map
|
||||
@@ -2,11 +2,24 @@ apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// (Disabled) Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
// cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
// bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../..")
|
||||
@@ -55,31 +68,12 @@ react {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
android {
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
version '3.22.1'
|
||||
}
|
||||
}
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
@@ -89,21 +83,17 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097788
|
||||
versionName "3.6.0"
|
||||
versionCode 2097800
|
||||
versionName "3.6.12"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||
multiDexEnabled true
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags '-DCMAKE_BUILD_TYPE=Release'
|
||||
// For 16 KB pages. This should be removable after upgrading to NDK r28
|
||||
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -129,19 +119,25 @@ android {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
minifyEnabled enableMinifyInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
profileable {
|
||||
// Release-like build that allows profiling with Android Studio Profiler
|
||||
initWith release
|
||||
signingConfig signingConfigs.debug
|
||||
// Required for Android Studio Profiler to attach
|
||||
debuggable false
|
||||
// Keeps symbols for better stack traces in profiler
|
||||
minifyEnabled false
|
||||
// Use release variants of dependencies that don't have profileable
|
||||
matchingFallbacks = ['release']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# Keep classes referenced by JNI
|
||||
# (see https://developer.android.com/topic/performance/app-optimization/add-keep-rules)
|
||||
-keep class com.margelo.nitro.whispervoicetyping.AudioRecorder
|
||||
|
||||
@@ -44,8 +44,12 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<!-- Enable profiling in release builds (Android 10+) -->
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<!--
|
||||
2018-12-16: Changed android:launchMode from "singleInstance" to "singleTop" for Firebase notification
|
||||
Previously singleInstance was necessary to prevent multiple instance of the RN app from running at the same time, but maybe no longer needed.
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html.
|
||||
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
|
||||
|
||||
# Sets the minimum CMake version required for this project.
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
|
||||
# Since this is the top level CMakeLists.txt, the project name is also accessible
|
||||
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
|
||||
# build script scope).
|
||||
project("joplin")
|
||||
|
||||
# Creates and names a library, sets it as either STATIC
|
||||
# or SHARED, and provides the relative paths to its source code.
|
||||
# You can define multiple libraries, and CMake builds them for you.
|
||||
# Gradle automatically packages shared libraries with your APK.
|
||||
#
|
||||
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
||||
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
||||
# is preferred for the same purpose.
|
||||
#
|
||||
# In order to load a library into your app from Java/Kotlin, you must call
|
||||
# System.loadLibrary() and pass the name of the library defined here;
|
||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||
# used in the AndroidManifest.xml file.
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
whisperWrapper.cpp
|
||||
utils/WhisperSession.cpp
|
||||
utils/findLongestSilence.cpp
|
||||
utils/findLongestSilence_test.cpp
|
||||
)
|
||||
|
||||
|
||||
|
||||
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../vendor/whisper.cpp)
|
||||
|
||||
# Based on the Whisper.cpp Android example:
|
||||
set(SHARED_FLAGS "-O3 ")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
|
||||
|
||||
# Whisper: See https://stackoverflow.com/a/76290722
|
||||
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)
|
||||
|
||||
# Directories for header files
|
||||
target_include_directories(
|
||||
${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${PROJECT_BASE_DIR}/shared
|
||||
${WHISPER_LIB_DIR}/include
|
||||
)
|
||||
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link libraries from various origins, such as libraries defined in this
|
||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
whisper
|
||||
# List libraries link to the target library
|
||||
android
|
||||
log
|
||||
)
|
||||
@@ -1,151 +0,0 @@
|
||||
// Write C++ code here.
|
||||
//
|
||||
// Do not forget to dynamically load the C++ library into your application.
|
||||
//
|
||||
// For instance,
|
||||
//
|
||||
// In MainActivity.java:
|
||||
// static {
|
||||
// System.loadLibrary("joplin");
|
||||
// }
|
||||
//
|
||||
// Or, in MainActivity.kt:
|
||||
// companion object {
|
||||
// init {
|
||||
// System.loadLibrary("joplin")
|
||||
// }
|
||||
// }
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <android/log.h>
|
||||
#include "whisper.h"
|
||||
#include "utils/WhisperSession.h"
|
||||
#include "utils/androidUtil.h"
|
||||
#include "utils/findLongestSilence_test.h"
|
||||
|
||||
void log_android(enum ggml_log_level level, const char* message, void* user_data) {
|
||||
android_LogPriority priority = level == 4 ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO;
|
||||
__android_log_print(priority, "Whisper::JNI::cpp", "%s", message);
|
||||
}
|
||||
|
||||
jstring stringToJava(JNIEnv *env, const std::string& source) {
|
||||
return env->NewStringUTF(source.c_str());
|
||||
}
|
||||
|
||||
std::string stringToCXX(JNIEnv *env, jstring jString) {
|
||||
const char *jStringChars = env->GetStringUTFChars(jString, nullptr);
|
||||
std::string result { jStringChars };
|
||||
env->ReleaseStringUTFChars(jString, jStringChars);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void throwException(JNIEnv *env, const std::string& message) {
|
||||
jclass errorClass = env->FindClass("java/lang/Exception");
|
||||
env->ThrowNew(errorClass, message.c_str());
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_init(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jstring modelPath,
|
||||
jstring language,
|
||||
jstring prompt,
|
||||
jboolean useShortAudioContext
|
||||
) {
|
||||
whisper_log_set(log_android, nullptr);
|
||||
|
||||
try {
|
||||
auto *pSession = new WhisperSession(
|
||||
stringToCXX(env, modelPath), stringToCXX(env, language), stringToCXX(env, prompt), useShortAudioContext
|
||||
);
|
||||
return (jlong) pSession;
|
||||
} catch (const std::exception& exception) {
|
||||
LOGW("Failed to init whisper: %s", exception.what());
|
||||
throwException(env, exception.what());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_free(JNIEnv *env, jobject thiz,
|
||||
jlong pointer) {
|
||||
delete reinterpret_cast<WhisperSession *>(pointer);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_addAudio(JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong pointer,
|
||||
jfloatArray audio_data) {
|
||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
||||
jfloat *pAudioData = env->GetFloatArrayElements(audio_data, nullptr);
|
||||
jsize lenAudioData = env->GetArrayLength(audio_data);
|
||||
std::string result;
|
||||
|
||||
try {
|
||||
pSession->addAudio(pAudioData, lenAudioData);
|
||||
} catch (const std::exception& exception) {
|
||||
LOGW("Failed to add to audio buffer: %s", exception.what());
|
||||
throwException(env, exception.what());
|
||||
}
|
||||
|
||||
// JNI_ABORT: "free the buffer without copying back the possible changes", pass 0 to copy
|
||||
// changes (there should be no changes)
|
||||
env->ReleaseFloatArrayElements(audio_data, pAudioData, JNI_ABORT);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeNextChunk(JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong pointer) {
|
||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
||||
std::string result;
|
||||
|
||||
try {
|
||||
result = pSession->transcribeNextChunk();
|
||||
} catch (const std::exception& exception) {
|
||||
LOGW("Failed to run whisper: %s", exception.what());
|
||||
throwException(env, exception.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return stringToJava(env, result);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeRemaining(JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong pointer) {
|
||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
||||
std::string result;
|
||||
|
||||
try {
|
||||
result = pSession->transcribeAll();
|
||||
} catch (const std::exception& exception) {
|
||||
LOGW("Failed to run whisper: %s", exception.what());
|
||||
throwException(env, exception.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return stringToJava(env, result);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_runTests(JNIEnv *env, jobject thiz) {
|
||||
try {
|
||||
findLongestSilence_test();
|
||||
} catch (const std::exception& exception) {
|
||||
LOGW("Failed to run tests: %s", exception.what());
|
||||
throwException(env, exception.what());
|
||||
}
|
||||
}
|
||||
@@ -6,37 +6,36 @@ import expo.modules.ReactNativeHostWrapper
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.common.ReleaseLevel
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import net.cozic.joplin.audio.SpeechToTextPackage
|
||||
import net.cozic.joplin.versioninfo.SystemVersionInformationPackage
|
||||
import net.cozic.joplin.share.SharePackage
|
||||
import net.cozic.joplin.ssl.SslPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
add(SharePackage())
|
||||
add(SslPackage())
|
||||
add(SystemVersionInformationPackage())
|
||||
add(SpeechToTextPackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
})
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
})
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(this.applicationContext, reactNativeHost)
|
||||
@@ -44,16 +43,17 @@ class MainApplication : Application(), ReactApplication {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
try {
|
||||
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.STABLE
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.cozic.joplin.audio
|
||||
|
||||
|
||||
class InvalidSessionIdException(id: Int) : IllegalArgumentException("Invalid session ID $id") {
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package net.cozic.joplin.audio
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
class NativeWhisperLib(
|
||||
modelPath: String,
|
||||
languageCode: String,
|
||||
prompt: String,
|
||||
shortAudioContext: Boolean,
|
||||
) : Closeable {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("joplin")
|
||||
}
|
||||
|
||||
external fun runTests(): Unit;
|
||||
|
||||
// TODO: The example whisper.cpp project transfers pointers as Longs to the Kotlin code.
|
||||
// This seems unsafe. Try changing how this is managed.
|
||||
private external fun init(modelPath: String, languageCode: String, prompt: String, shortAudioContext: Boolean): Long;
|
||||
private external fun free(pointer: Long): Unit;
|
||||
|
||||
private external fun addAudio(pointer: Long, audioData: FloatArray): Unit;
|
||||
private external fun transcribeNextChunk(pointer: Long): String;
|
||||
private external fun transcribeRemaining(pointer: Long): String;
|
||||
}
|
||||
|
||||
private var closed = false
|
||||
private val pointer: Long = init(modelPath, languageCode, prompt, shortAudioContext)
|
||||
|
||||
fun addAudio(audioData: FloatArray) {
|
||||
if (closed) {
|
||||
throw Exception("Cannot add audio data to a closed session")
|
||||
}
|
||||
|
||||
Companion.addAudio(pointer, audioData)
|
||||
}
|
||||
|
||||
fun transcribeNextChunk(): String {
|
||||
if (closed) {
|
||||
throw Exception("Cannot transcribe using a closed session")
|
||||
}
|
||||
|
||||
return Companion.transcribeNextChunk(pointer)
|
||||
}
|
||||
|
||||
fun transcribeRemaining(): String {
|
||||
if (closed) {
|
||||
throw Exception("Cannot transcribeAll using a closed session")
|
||||
}
|
||||
|
||||
return Companion.transcribeRemaining(pointer)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (closed) {
|
||||
throw Exception("Cannot close a whisper session twice")
|
||||
}
|
||||
|
||||
closed = true
|
||||
free(pointer)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package net.cozic.joplin.audio
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.Closeable
|
||||
|
||||
class SpeechToTextConverter(
|
||||
modelPath: String,
|
||||
locale: String,
|
||||
prompt: String,
|
||||
useShortAudioCtx: Boolean,
|
||||
recorderFactory: AudioRecorderFactory,
|
||||
context: Context,
|
||||
) : Closeable {
|
||||
private val recorder = recorderFactory(context)
|
||||
private val languageCode = Regex("_.*").replace(locale, "")
|
||||
private var whisper = NativeWhisperLib(
|
||||
modelPath,
|
||||
languageCode,
|
||||
prompt,
|
||||
useShortAudioCtx,
|
||||
)
|
||||
|
||||
fun start() {
|
||||
recorder.start()
|
||||
}
|
||||
|
||||
private fun convert(data: FloatArray): String {
|
||||
Log.d("Whisper", "Pre-transcribe data of size ${data.size}")
|
||||
whisper.addAudio(data)
|
||||
val result = whisper.transcribeNextChunk()
|
||||
Log.d("Whisper", "Post transcribe. Got $result")
|
||||
return result;
|
||||
}
|
||||
|
||||
fun dropFirstSeconds(seconds: Double) {
|
||||
Log.i("Whisper", "Drop first seconds $seconds")
|
||||
recorder.dropFirstSeconds(seconds)
|
||||
}
|
||||
|
||||
val bufferLengthSeconds: Double get() = recorder.bufferLengthSeconds
|
||||
|
||||
fun convertNext(seconds: Double): String {
|
||||
val buffer = recorder.pullNextSeconds(seconds)
|
||||
val result = convert(buffer)
|
||||
dropFirstSeconds(seconds)
|
||||
return result
|
||||
}
|
||||
|
||||
// Converts as many seconds of buffered data as possible, without waiting
|
||||
fun convertRemaining(): String {
|
||||
val buffer = recorder.pullAvailable()
|
||||
whisper.addAudio(buffer)
|
||||
return whisper.transcribeRemaining()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.d("Whisper", "Close")
|
||||
recorder.close()
|
||||
whisper.close()
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package net.cozic.joplin.audio
|
||||
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class SpeechToTextPackage : ReactPackage {
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||
return listOf<NativeModule>(SpeechToTextModule(reactContext))
|
||||
}
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
class SpeechToTextModule(
|
||||
private var context: ReactApplicationContext,
|
||||
) : ReactContextBaseJavaModule(context), LifecycleEventListener {
|
||||
private val executorService: ExecutorService = Executors.newFixedThreadPool(1)
|
||||
private val sessionManager = SpeechToTextSessionManager(executorService)
|
||||
|
||||
override fun getName() = "SpeechToTextModule"
|
||||
|
||||
override fun onHostResume() { }
|
||||
override fun onHostPause() { }
|
||||
override fun onHostDestroy() { }
|
||||
|
||||
@ReactMethod
|
||||
fun runTests(promise: Promise) {
|
||||
try {
|
||||
NativeWhisperLib.runTests()
|
||||
promise.resolve(true)
|
||||
} catch (exception: Throwable) {
|
||||
promise.reject(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun openSession(modelPath: String, locale: String, prompt: String, useShortAudioCtx: Boolean, promise: Promise) {
|
||||
val appContext = context.applicationContext
|
||||
|
||||
try {
|
||||
val sessionId = sessionManager.openSession(modelPath, locale, prompt, useShortAudioCtx, appContext)
|
||||
promise.resolve(sessionId)
|
||||
} catch (exception: Throwable) {
|
||||
promise.reject(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun startRecording(sessionId: Int, promise: Promise) {
|
||||
sessionManager.startRecording(sessionId, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
|
||||
sessionManager.getBufferLengthSeconds(sessionId, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
|
||||
sessionManager.dropFirstSeconds(sessionId, duration, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
|
||||
sessionManager.convertNext(sessionId, duration, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun convertAvailable(sessionId: Int, promise: Promise) {
|
||||
sessionManager.convertAvailable(sessionId, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun closeSession(sessionId: Int, promise: Promise) {
|
||||
sessionManager.closeSession(sessionId, promise)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package net.cozic.joplin.audio
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.react.bridge.Promise
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
class SpeechToTextSession (
|
||||
val converter: SpeechToTextConverter
|
||||
) {
|
||||
val mutex = ReentrantLock()
|
||||
}
|
||||
|
||||
class SpeechToTextSessionManager(
|
||||
private var executor: Executor,
|
||||
) {
|
||||
private val sessions: MutableMap<Int, SpeechToTextSession> = mutableMapOf()
|
||||
private var nextSessionId: Int = 0
|
||||
|
||||
fun openSession(
|
||||
modelPath: String,
|
||||
locale: String,
|
||||
prompt: String,
|
||||
useShortAudioCtx: Boolean,
|
||||
context: Context,
|
||||
): Int {
|
||||
val sessionId = nextSessionId++
|
||||
sessions[sessionId] = SpeechToTextSession(
|
||||
SpeechToTextConverter(
|
||||
modelPath, locale, prompt, useShortAudioCtx, recorderFactory = AudioRecorder.factory, context,
|
||||
)
|
||||
)
|
||||
return sessionId
|
||||
}
|
||||
|
||||
private fun getSession(id: Int): SpeechToTextSession {
|
||||
return sessions[id] ?: throw InvalidSessionIdException(id)
|
||||
}
|
||||
|
||||
private fun concurrentWithSession(
|
||||
id: Int,
|
||||
callback: (session: SpeechToTextSession)->Unit,
|
||||
) {
|
||||
executor.execute {
|
||||
val session = getSession(id)
|
||||
session.mutex.lock()
|
||||
try {
|
||||
callback(session)
|
||||
} finally {
|
||||
session.mutex.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun concurrentWithSession(
|
||||
id: Int,
|
||||
onError: (error: Throwable)->Unit,
|
||||
callback: (session: SpeechToTextSession)->Unit,
|
||||
) {
|
||||
return concurrentWithSession(id) { session ->
|
||||
try {
|
||||
callback(session)
|
||||
} catch (error: Throwable) {
|
||||
onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startRecording(sessionId: Int, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
||||
session.converter.start()
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Left-shifts the recording buffer by [duration] seconds
|
||||
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
||||
session.converter.dropFirstSeconds(duration)
|
||||
promise.resolve(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
||||
promise.resolve(session.converter.bufferLengthSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
// Waits for the next [duration] seconds to become available, then converts
|
||||
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
||||
val result = session.converter.convertNext(duration)
|
||||
promise.resolve(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Converts all available recorded data
|
||||
fun convertAvailable(sessionId: Int, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
||||
val result = session.converter.convertRemaining()
|
||||
promise.resolve(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeSession(sessionId: Int, promise: Promise) {
|
||||
this.concurrentWithSession(sessionId) { session ->
|
||||
session.converter.close()
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "35.0.0"
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.0.21"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
@@ -34,12 +34,17 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=true
|
||||
|
||||
# To fix this error:
|
||||
#
|
||||
# > Failed to transform bcprov-jdk15on-1.68.jar
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
4
packages/app-mobile/android/gradlew
vendored
4
packages/app-mobile/android/gradlew
vendored
@@ -114,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
4
packages/app-mobile/android/gradlew.bat
vendored
4
packages/app-mobile/android/gradlew.bat
vendored
@@ -70,11 +70,11 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
whisper.cpp/.gitmodules
|
||||
whisper.cpp/scripts/
|
||||
whisper.cpp/samples/
|
||||
whisper.cpp/tests/
|
||||
whisper.cpp/models/
|
||||
whisper.cpp/examples/
|
||||
whisper.cpp/.*/
|
||||
whisper.cpp/bindings/
|
||||
whisper.cpp/**/*.Dockerfile
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"displayName": "Joplin"
|
||||
}
|
||||
"displayName": "Joplin",
|
||||
"plugins": [
|
||||
"@react-native-community/datetimepicker"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ const useStyles = (theme: ThemeStyle) => {
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 20,
|
||||
paddingBottom: 14,
|
||||
paddingBottom: 14 + safeAreaPadding.paddingBottom,
|
||||
gap: 8,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
@@ -438,9 +438,8 @@ const useInputEventHandlers = ({
|
||||
const onSubmit = useCallback(() => {
|
||||
if (selectedResult) {
|
||||
onItemSelected(selectedResult, selectedIndex);
|
||||
setSearch('');
|
||||
}
|
||||
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
|
||||
}, [onItemSelected, selectedResult, selectedIndex]);
|
||||
|
||||
// For now, onKeyPress only works on web.
|
||||
// See https://github.com/react-native-community/discussions-and-proposals/issues/249
|
||||
|
||||
@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Props, WebViewControl } from './types';
|
||||
import useCss from './utils/useCss';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
@@ -141,7 +142,8 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
onLoadEnd={props.onLoadEnd}
|
||||
onContentProcessDidTerminate={refreshWebViewAfterCrash}
|
||||
onRenderProcessGone={refreshWebViewAfterCrash}
|
||||
decelerationRate='normal'
|
||||
// See https://github.com/react-native-webview/react-native-webview/issues/3814
|
||||
decelerationRate={Platform.OS === 'ios' ? 'normal' : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ import * as React from 'react';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../utils/types';
|
||||
import TestProviderStack from './testing/TestProviderStack';
|
||||
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch, waitFor } from '@joplin/lib/testing/test-utils';
|
||||
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch } from '@joplin/lib/testing/test-utils';
|
||||
import waitFor from '@joplin/lib/testing/waitFor';
|
||||
import createMockReduxStore from '../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../utils/testing/setupGlobalStore';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
||||
|
||||
26
packages/app-mobile/components/KeyboardAvoidingView.tsx
Normal file
26
packages/app-mobile/components/KeyboardAvoidingView.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { KeyboardAvoidingViewProps, KeyboardAvoidingView as NativeKeyboardAvoidingView } from 'react-native';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
|
||||
interface Props extends KeyboardAvoidingViewProps {}
|
||||
|
||||
const KeyboardAvoidingView: React.FC<Props> = ({ enabled, children, ...forwardedProps }) => {
|
||||
const keyboardState = useKeyboardState();
|
||||
|
||||
enabled &&= (
|
||||
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
|
||||
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
|
||||
// See https://github.com/facebook/react-native/issues/29473
|
||||
!keyboardState.isFloatingKeyboard
|
||||
);
|
||||
|
||||
return <NativeKeyboardAvoidingView
|
||||
behavior='padding'
|
||||
{...forwardedProps}
|
||||
enabled={enabled}
|
||||
>
|
||||
{children}
|
||||
</NativeKeyboardAvoidingView>;
|
||||
};
|
||||
|
||||
export default KeyboardAvoidingView;
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { GestureResponderEvent, KeyboardAvoidingView, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import FocusControl from './accessibility/FocusControl/FocusControl';
|
||||
import { msleep, Second } from '@joplin/utils/time';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ModalState } from './accessibility/FocusControl/types';
|
||||
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import KeyboardAvoidingView from './KeyboardAvoidingView';
|
||||
|
||||
export interface ModalElementProps extends ModalProps {
|
||||
children: React.ReactNode;
|
||||
@@ -175,7 +176,7 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
{...modalProps}
|
||||
>
|
||||
{scrollOverflow ? (
|
||||
<KeyboardAvoidingView behavior='padding' style={styles.keyboardAvoidingView}>
|
||||
<KeyboardAvoidingView style={styles.keyboardAvoidingView} enabled={true}>
|
||||
<ScrollView
|
||||
{...extraScrollViewProps}
|
||||
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
||||
|
||||
@@ -61,7 +61,7 @@ const ProfileListItem: React.FC<ProfileItemProps> = ({ profile, profileConfig, s
|
||||
}
|
||||
};
|
||||
|
||||
const switchProfileMessage = _('To switch the profile, the app is going to close and you will need to restart it.');
|
||||
const switchProfileMessage = _('To switch the profile, the app is going to restart.');
|
||||
if (shim.mobilePlatform() === 'web') {
|
||||
if (confirm(switchProfileMessage)) {
|
||||
void doIt();
|
||||
|
||||
@@ -688,16 +688,23 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
const menuComp =
|
||||
!menuOptions.length || !showContextMenuButton ? null : (
|
||||
<Menu themeId={this.props.themeId} options={menuOptions}>
|
||||
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
|
||||
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
|
||||
<View style={contextMenuStyle}>
|
||||
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={_('Actions')}/>
|
||||
</View>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// Updating the state of this component can result in the left most element becoming hidden, so add a dummy as the first element to prevent this
|
||||
// See https://github.com/laurent22/joplin/issues/14153
|
||||
const zeroWidthSpacer = (
|
||||
<View style={{ width: 0 }} pointerEvents="none"/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.styles().outerContainer}>
|
||||
<View style={this.styles().aboveHeader}/>
|
||||
<View style={this.styles().innerContainer}>
|
||||
{zeroWidthSpacer}
|
||||
{sideMenuComp}
|
||||
{backButtonComp}
|
||||
{renderUndoButton()}
|
||||
|
||||
@@ -7,6 +7,7 @@ import AccessibleView from './accessibility/AccessibleView';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
|
||||
import { themeStyle } from './global-style';
|
||||
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||
|
||||
export enum SideMenuPosition {
|
||||
Left = 'left',
|
||||
@@ -40,6 +41,8 @@ interface UseStylesProps {
|
||||
|
||||
const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStylesProps) => {
|
||||
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
||||
const safeAreaInsets = useSafeAreaPadding();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
@@ -53,7 +56,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
||||
contentOuterWrapper: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
width: windowWidth,
|
||||
width: '100%',
|
||||
height: windowHeight,
|
||||
transform: [{
|
||||
translateX: menuOpenFraction.interpolate({
|
||||
@@ -71,11 +74,18 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
||||
flexShrink: 1,
|
||||
},
|
||||
menuWrapper: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: menuWidth,
|
||||
|
||||
paddingLeft: isLeftMenu ? safeAreaInsets.paddingLeft : 0,
|
||||
paddingRight: isLeftMenu ? 0 : safeAreaInsets.paddingRight,
|
||||
paddingTop: safeAreaInsets.paddingTop,
|
||||
paddingBottom: safeAreaInsets.paddingBottom,
|
||||
|
||||
// In React Native, RTL replaces `left` with `right` and `right` with `left`.
|
||||
// As such, we need to reverse the normal direction in RTL mode.
|
||||
...(isLeftMenu === !I18nManager.isRTL ? {
|
||||
@@ -107,7 +117,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
||||
width: windowWidth,
|
||||
},
|
||||
});
|
||||
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction]);
|
||||
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction, safeAreaInsets]);
|
||||
};
|
||||
|
||||
interface UseAnimationsProps {
|
||||
|
||||
@@ -2,13 +2,16 @@ import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NotesScreen from './screens/Notes/Notes';
|
||||
import SearchScreen from './screens/SearchScreen';
|
||||
import { KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||
import { Platform, View, StyleSheet } from 'react-native';
|
||||
import { AppState } from '../utils/types';
|
||||
import { themeStyle } from './global-style';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import FeedbackBanner from './FeedbackBanner';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useMemo } from 'react';
|
||||
import KeyboardAvoidingView from './KeyboardAvoidingView';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -20,6 +23,15 @@ interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const useStyles = (theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
keyboardAvoidingView: { flex: 1, backgroundColor: theme.backgroundColor },
|
||||
});
|
||||
}, [theme]);
|
||||
};
|
||||
|
||||
|
||||
const AppNavComponent: React.FC<Props> = (props) => {
|
||||
const keyboardState = useKeyboardState();
|
||||
const safeAreaPadding = useSafeAreaInsets();
|
||||
@@ -50,20 +62,18 @@ const AppNavComponent: React.FC<Props> = (props) => {
|
||||
const searchScreenLoaded = searchScreenVisible || (previousRouteName === 'Search' && route.routeName === 'Note');
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const style = { flex: 1, backgroundColor: theme.backgroundColor };
|
||||
|
||||
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
|
||||
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
|
||||
// See https://github.com/facebook/react-native/issues/29473
|
||||
const keyboardAvoidingViewEnabled = !keyboardState.isFloatingKeyboard;
|
||||
const autocompletionBarPadding = Platform.OS === 'ios' && keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
|
||||
const styles = useStyles(theme);
|
||||
const autocompletionBarPadding = keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
enabled={keyboardAvoidingViewEnabled}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : null}
|
||||
style={style}
|
||||
style={styles.keyboardAvoidingView}
|
||||
enabled={
|
||||
// Workaround: On Android 15 and 16, the main app content seems to auto-resize when the keyboard is shown.
|
||||
// On earlier Android versions (and in modals), this does not seem to be the case.
|
||||
(Platform.OS === 'android' && Platform.Version < 35)
|
||||
|| Platform.OS === 'ios'
|
||||
}
|
||||
>
|
||||
<NotesScreen visible={notesScreenVisible} />
|
||||
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
|
||||
|
||||
@@ -16,6 +16,7 @@ import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
|
||||
import AccessibleView from '../accessibility/AccessibleView';
|
||||
import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
|
||||
import { ViewStyle } from 'react-native';
|
||||
|
||||
const logger = Logger.create('PluginRunnerWebView');
|
||||
|
||||
@@ -98,6 +99,17 @@ interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
// The WebView needs to have a non-zero size to be rendered by
|
||||
// newer React Native versions. This style makes it visually hidden.
|
||||
const hiddenStyle: ViewStyle = {
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
zIndex: -1,
|
||||
};
|
||||
|
||||
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
@@ -189,7 +201,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessibleView style={{ display: 'none' }} inert={true}>
|
||||
<AccessibleView style={hiddenStyle} inert={true}>
|
||||
{renderWebView()}
|
||||
</AccessibleView>
|
||||
);
|
||||
|
||||
@@ -739,6 +739,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
return false;
|
||||
};
|
||||
|
||||
private handleSettingButtonPress = async (key: string) => {
|
||||
if (key === 'sync.6.oidcLogin') {
|
||||
// Save current settings before navigating to login
|
||||
await shared.saveSettings(this);
|
||||
await NavService.go('WebDavOidcLogin');
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public settingToComponent(key: string, value: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -755,6 +763,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
themeId={this.props.themeId}
|
||||
updateSettingValue={updateSettingValue}
|
||||
styles={this.styles()}
|
||||
onSettingButtonPress={this.handleSettingButtonPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { View, Text } from 'react-native';
|
||||
import { View, Text, Button } from 'react-native';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import Dropdown from '../../Dropdown';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
themeId: number;
|
||||
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
onSettingButtonPress?: (key: string)=> void;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +128,21 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
|
||||
/>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
// TODO: Not yet supported
|
||||
return (
|
||||
<View key={props.settingId} style={containerStyles.outerContainer}>
|
||||
<View style={containerStyles.innerContainer}>
|
||||
<Button
|
||||
title={md.label()}
|
||||
onPress={() => {
|
||||
if (props.onSettingButtonPress) {
|
||||
props.onSettingButtonPress(props.settingId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (Setting.value('env') === 'dev') {
|
||||
throw new Error(`Unsupported setting type: ${md.type}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../uti
|
||||
|
||||
import NoteScreen from './Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
|
||||
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
|
||||
import waitForWithRealTimers from '@joplin/lib/testing/waitFor';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
|
||||
const { View, StyleSheet } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { default: FolderPicker } = require('../FolderPicker');
|
||||
const TextInput = require('../TextInput').default;
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import { ScreenHeader } from '../ScreenHeader';
|
||||
import { BaseScreenComponent } from '../base-screen';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import FolderPicker from '../FolderPicker';
|
||||
import TextInput from '../TextInput';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
class FolderScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
interface Props {
|
||||
folderId: string;
|
||||
selectedFolderId: string;
|
||||
themeId: number;
|
||||
folders: FolderEntity[];
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface State {
|
||||
folder: FolderEntity;
|
||||
lastSavedFolder: FolderEntity|null;
|
||||
}
|
||||
|
||||
class FolderScreenComponent extends BaseScreenComponent<Props, State> {
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folder: Folder.new(),
|
||||
lastSavedFolder: null,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
public override UNSAFE_componentWillMount() {
|
||||
if (!this.props.folderId) {
|
||||
const folder = Folder.new();
|
||||
this.setState({
|
||||
@@ -33,7 +46,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
Folder.load(this.props.folderId).then(folder => {
|
||||
void Folder.load(this.props.folderId).then(folder => {
|
||||
this.setState({
|
||||
folder: folder,
|
||||
lastSavedFolder: { ...folder },
|
||||
@@ -42,38 +55,40 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
isModified() {
|
||||
private isModified() {
|
||||
if (!this.state.folder || !this.state.lastSavedFolder) return false;
|
||||
const diff = BaseModel.diffObjects(this.state.folder, this.state.lastSavedFolder);
|
||||
delete diff.type_;
|
||||
return !!Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
folderComponent_change(propName, propValue) {
|
||||
private folderComponent_change(propName: keyof FolderEntity, propValue: string) {
|
||||
this.setState((prevState) => {
|
||||
const folder = { ...prevState.folder };
|
||||
folder[propName] = propValue;
|
||||
const folder = {
|
||||
...prevState.folder,
|
||||
[propName]: propValue,
|
||||
};
|
||||
return { folder: folder };
|
||||
});
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
private title_changeText(text: string) {
|
||||
this.folderComponent_change('title', text);
|
||||
}
|
||||
|
||||
parent_changeValue(parent) {
|
||||
private parent_changeValue(parent: string) {
|
||||
this.folderComponent_change('parent_id', parent);
|
||||
}
|
||||
|
||||
|
||||
async saveFolderButton_press() {
|
||||
private async saveFolderButton_press() {
|
||||
let folder = { ...this.state.folder };
|
||||
|
||||
try {
|
||||
if (folder.id && !(await Folder.canNestUnder(folder.id, folder.parent_id))) throw new Error(_('Cannot move notebook to this location'));
|
||||
folder = await Folder.save(folder, { userSideValidation: true });
|
||||
} catch (error) {
|
||||
shim.showErrorDialog(_('The notebook could not be saved: %s', error.message));
|
||||
void shim.showErrorDialog(_('The notebook could not be saved: %s', error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,7 +104,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public override render() {
|
||||
const saveButtonDisabled = !this.isModified() || !this.state.folder.title;
|
||||
|
||||
return (
|
||||
@@ -101,7 +116,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
autoFocus={true}
|
||||
value={this.state.folder.title}
|
||||
onChangeText={text => this.title_changeText(text)}
|
||||
disabled={this.state.folder.encryption_applied}
|
||||
editable={!this.state.folder.encryption_applied}
|
||||
/>
|
||||
<View style={styles.folderPickerContainer}>
|
||||
<FolderPicker
|
||||
@@ -120,7 +135,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const FolderScreen = connect(state => {
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
folderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
@@ -138,4 +153,3 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { FolderScreen };
|
||||
158
packages/app-mobile/components/screens/webdav-oidc-login.tsx
Normal file
158
packages/app-mobile/components/screens/webdav-oidc-login.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { View, Button } from 'react-native';
|
||||
import { WebView, WebViewNavigation } from 'react-native-webview';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ScreenHeader } from '../ScreenHeader';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { themeStyle } from '../global-style';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import OidcApi from '@joplin/lib/OidcApi';
|
||||
const parseUri = require('@joplin/lib/parseUri');
|
||||
|
||||
const WebDavOidcLoginScreen: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const themeId = useSelector((state: { settings: { theme: number } }) => state.settings.theme);
|
||||
|
||||
const [webviewUrl, setWebviewUrl] = useState('');
|
||||
const [oidcApi, setOidcApi] = useState<OidcApi | null>(null);
|
||||
const [redirectUri, setRedirectUri] = useState('');
|
||||
const [oauthState, setOauthState] = useState('');
|
||||
|
||||
const authCodeRef = useRef<string | null>(null);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return {
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
};
|
||||
}, [themeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const initOidc = async () => {
|
||||
const api = new OidcApi({
|
||||
issuerUrl: Setting.value('sync.6.oidcIssuerUrl'),
|
||||
clientId: Setting.value('sync.6.oidcClientId'),
|
||||
clientSecret: Setting.value('sync.6.oidcClientSecret'),
|
||||
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
|
||||
});
|
||||
|
||||
// Use a custom redirect URI that the WebView can intercept
|
||||
// This is a common pattern for mobile OAuth - using a non-http URI
|
||||
const redirect = 'joplin://oidc-callback';
|
||||
const state = Math.random().toString(36).substring(7);
|
||||
|
||||
const authCodeUrl = await api.authCodeUrl(redirect, state);
|
||||
|
||||
setOidcApi(api);
|
||||
setRedirectUri(redirect);
|
||||
setOauthState(state);
|
||||
setWebviewUrl(authCodeUrl);
|
||||
};
|
||||
|
||||
void initOidc();
|
||||
}, []);
|
||||
|
||||
const handleWebviewLoad = useCallback(async (event: WebViewNavigation) => {
|
||||
const url = event.url;
|
||||
|
||||
// Check if this is our callback URL
|
||||
if (url.startsWith('joplin://oidc-callback')) {
|
||||
const parsedUrl = parseUri(url);
|
||||
const query = parsedUrl.queryKey;
|
||||
|
||||
if (query.error) {
|
||||
const errorDesc = query.error_description || query.error;
|
||||
alert(`${_('Authentication failed')}: ${errorDesc}`);
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authCodeRef.current && query.code) {
|
||||
// Verify state to prevent CSRF
|
||||
if (query.state !== oauthState) {
|
||||
alert(_('Authentication failed: Invalid state parameter'));
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
return;
|
||||
}
|
||||
|
||||
authCodeRef.current = query.code;
|
||||
|
||||
try {
|
||||
await oidcApi.execTokenRequest(authCodeRef.current, redirectUri);
|
||||
const auth = oidcApi.auth();
|
||||
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
Setting.setValue(`sync.${syncTargetId}.oidcAuth`, auth ? JSON.stringify(auth) : '');
|
||||
|
||||
// Update the sync target's API with the new auth
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
if (syncTarget.api && syncTarget.api()) {
|
||||
syncTarget.api().setAuth(auth);
|
||||
}
|
||||
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
void reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
alert(`${_('Could not authenticate with OIDC provider. Please try again')}\n\n${(error as Error).message}`);
|
||||
}
|
||||
|
||||
authCodeRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [dispatch, oidcApi, oauthState, redirectUri]);
|
||||
|
||||
const handleWebviewError = useCallback(() => {
|
||||
alert(_('Could not load page. Please check your connection and try again.'));
|
||||
}, []);
|
||||
|
||||
const handleRetryPress = useCallback(() => {
|
||||
// Reload the page by setting a temporary URL then back to the auth URL
|
||||
const authUrl = webviewUrl;
|
||||
|
||||
setWebviewUrl('about:blank');
|
||||
|
||||
shim.setTimeout(() => {
|
||||
setWebviewUrl(authUrl);
|
||||
}, 500);
|
||||
}, [webviewUrl]);
|
||||
|
||||
const handleShouldStartLoadWithRequest = useCallback((request: { url: string }) => {
|
||||
// Intercept the callback URL
|
||||
if (request.url.startsWith('joplin://oidc-callback')) {
|
||||
void handleWebviewLoad({ url: request.url } as WebViewNavigation);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [handleWebviewLoad]);
|
||||
|
||||
const source = useMemo(() => ({ uri: webviewUrl }), [webviewUrl]);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<ScreenHeader title={_('WebDAV OIDC Login')} />
|
||||
<WebView
|
||||
source={source}
|
||||
onNavigationStateChange={(event: WebViewNavigation) => {
|
||||
void handleWebviewLoad(event);
|
||||
}}
|
||||
onError={handleWebviewError}
|
||||
onHttpError={handleWebviewError}
|
||||
// Allow the custom joplin:// scheme to be intercepted
|
||||
originWhitelist={['*']}
|
||||
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
||||
/>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={handleRetryPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebDavOidcLoginScreen;
|
||||
@@ -6,16 +6,14 @@
|
||||
|
||||
// So there's basically still a one way flux: React => SQLite => Redux => React
|
||||
|
||||
import './utils/initReact';
|
||||
import './utils/polyfills';
|
||||
|
||||
import Root from './root';
|
||||
import { LogBox } from 'react-native';
|
||||
import { registerRootComponent } from 'expo';
|
||||
// Allows loading image assets. See https://github.com/expo/expo/issues/31240
|
||||
import 'expo-asset';
|
||||
import shim from '@joplin/lib/shim';
|
||||
shim.setReact(require('react'));
|
||||
|
||||
const Root = require('./root').default;
|
||||
|
||||
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
|
||||
// or don't really matter. Because we want important warnings to actually be fixed, we disable
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import './utils/polyfills';
|
||||
import './utils/initReact';
|
||||
import { AppRegistry } from 'react-native';
|
||||
import Root from './root';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
@@ -321,6 +321,8 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
@@ -339,12 +341,12 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactNativeFs/RNFS_PrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
||||
@@ -358,12 +360,12 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNFS_PrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
@@ -407,11 +409,15 @@
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -450,11 +456,16 @@
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/Joplin/Joplin.entitlements",
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/expo-configure-project.sh",
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
@@ -509,7 +520,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
@@ -518,7 +529,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MARKETING_VERSION = 13.6.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -544,7 +555,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
@@ -552,7 +563,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MARKETING_VERSION = 13.6.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -651,6 +662,7 @@
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -727,6 +739,7 @@
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
@@ -745,7 +758,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -756,7 +769,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MARKETING_VERSION = 13.6.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
@@ -788,7 +801,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -799,7 +812,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MARKETING_VERSION = 13.6.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -44,16 +44,15 @@
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
|
||||
<!-- Left over from before upgrading from RN 0.71, 0.73 -->
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<key>api.joplincloud.local</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>api.joplincloud.local</key>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
@@ -62,18 +61,22 @@
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>To allow attaching a photo to a note</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>To allow attaching voice recordings to a note</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>The images will be displayed on your notes.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>To allow attaching images to a note</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>To allow attaching voice recordings to a note</string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>AntDesign.ttf</string>
|
||||
@@ -86,6 +89,10 @@
|
||||
<string>MaterialDesignIcons.ttf</string>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
@@ -109,11 +116,5 @@
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -5,23 +5,33 @@ require File.join(File.dirname(`node --print "require.resolve('react-native/pack
|
||||
require 'json'
|
||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
def ccache_enabled?(podfile_properties)
|
||||
# Environment variable takes precedence
|
||||
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
||||
|
||||
# Fall back to Podfile properties
|
||||
podfile_properties['apple.ccacheEnabled'] == 'true'
|
||||
end
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||
install! 'cocoapods',
|
||||
:deterministic_uuids => false
|
||||
|
||||
prepare_react_native_project!
|
||||
|
||||
target 'Joplin' do
|
||||
use_expo_modules!
|
||||
|
||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] != '0'
|
||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||
else
|
||||
config_command = [
|
||||
'npx',
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(\'expo/bin/autolinking\')',
|
||||
'expo-modules-autolinking',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
@@ -35,13 +45,14 @@ target 'Joplin' do
|
||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||
)
|
||||
|
||||
|
||||
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
|
||||
|
||||
post_install do |installer|
|
||||
@@ -50,7 +61,7 @@ target 'Joplin' do
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"newArchEnabled": "false"
|
||||
"newArchEnabled": "true"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,7 @@
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -84,6 +84,7 @@ const emptyMockPackages = [
|
||||
'@joplin/react-native-saf-x',
|
||||
'expo-av',
|
||||
'expo-av/build/Audio',
|
||||
'expo-image-manipulator',
|
||||
];
|
||||
for (const packageName of emptyMockPackages) {
|
||||
jest.doMock(packageName, () => {
|
||||
@@ -130,7 +131,7 @@ mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
|
||||
// Use a temporary folder instead.
|
||||
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
|
||||
|
||||
jest.doMock('react-native-fs', () => {
|
||||
jest.doMock('@dr.pogodin/react-native-fs', () => {
|
||||
return {
|
||||
CachesDirectoryPath: tempDirectoryPath,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ const localPackages = {
|
||||
'@joplin/tools': path.resolve(__dirname, '../tools/'),
|
||||
'@joplin/utils': path.resolve(__dirname, '../utils/'),
|
||||
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
|
||||
'@joplin/whisper-voice-typing': path.resolve(__dirname, '../whisper-voice-typing/'),
|
||||
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
||||
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
|
||||
|
||||
@@ -21,25 +21,26 @@
|
||||
"postinstall": "jetify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@dr.pogodin/react-native-fs": "2.36.2",
|
||||
"@joplin/editor": "~3.6",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/react-native-alarm-notification": "~3.6",
|
||||
"@joplin/react-native-saf-x": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@joplin/whisper-voice-typing": "~3.6",
|
||||
"@js-draw/material-icons": "1.33.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.5",
|
||||
"@react-native-community/datetimepicker": "8.4.7",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-documents/picker": "10.1.7",
|
||||
"@react-native-vector-icons/fontawesome5": "12.3.0",
|
||||
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch",
|
||||
"@react-native-vector-icons/get-image": "12.3.0",
|
||||
"@react-native-vector-icons/ionicons": "12.3.0",
|
||||
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||
"@react-native-vector-icons/material-icons": "12.4.0",
|
||||
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch",
|
||||
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch",
|
||||
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
@@ -47,40 +48,41 @@
|
||||
"crypto-browserify": "3.12.1",
|
||||
"deprecated-react-native-prop-types": "5.0.0",
|
||||
"events": "3.3.0",
|
||||
"expo": "53.0.23",
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"expo": "54.0.31",
|
||||
"expo-av": "16.0.8",
|
||||
"expo-camera": "17.0.10",
|
||||
"expo-image-manipulator": "14.0.8",
|
||||
"expo-local-authentication": "17.0.8",
|
||||
"js-draw": "1.33.0",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
"path-browserify": "1.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"punycode": "2.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-device-info": "14.1.1",
|
||||
"react-native-dropdownalert": "5.2.0",
|
||||
"react-native-exit-app": "2.0.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-image-picker": "8.2.1",
|
||||
"react-native-localize": "3.5.4",
|
||||
"react-native-modal-datetime-picker": "18.0.0",
|
||||
"react-native-nitro-modules": "0.33.2",
|
||||
"react-native-paper": "5.14.5",
|
||||
"react-native-popup-menu": "0.17.0",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-quick-base64": "2.2.2",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.2.0",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-webview": "13.16.0",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-zip-archive": "7.0.2",
|
||||
"react-redux": "8.1.3",
|
||||
"redux": "4.2.1",
|
||||
@@ -101,26 +103,26 @@
|
||||
"@joplin/turndown": "~4.0.80",
|
||||
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||
"@react-native-community/cli": "16.0.3",
|
||||
"@react-native-community/cli-platform-android": "16.0.3",
|
||||
"@react-native-community/cli-platform-ios": "16.0.3",
|
||||
"@react-native/babel-preset": "0.80.1",
|
||||
"@react-native/metro-config": "0.79.5",
|
||||
"@react-native/typescript-config": "0.80.2",
|
||||
"@react-native-community/cli": "20.0.0",
|
||||
"@react-native-community/cli-platform-android": "20.0.0",
|
||||
"@react-native-community/cli-platform-ios": "20.0.0",
|
||||
"@react-native/babel-preset": "0.81.5",
|
||||
"@react-native/metro-config": "0.81.5",
|
||||
"@react-native/typescript-config": "0.81.5",
|
||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||
"@testing-library/react-native": "13.2.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.158",
|
||||
"@types/serviceworker": "0.0.165",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.21.2",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild": "0.25.12",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"gulp": "4.0.2",
|
||||
@@ -130,11 +132,11 @@
|
||||
"jsdom": "26.1.0",
|
||||
"nodemon": "3.1.10",
|
||||
"punycode": "2.3.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native-web": "0.21.2",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.4",
|
||||
"react-refresh": "0.18.0",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"sharp": "0.34.5",
|
||||
"sqlite3": "5.1.6",
|
||||
"timers-browserify": "2.0.12",
|
||||
"ts-jest": "29.4.1",
|
||||
@@ -147,7 +149,7 @@
|
||||
"webpack-dev-server": "5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"expo": {
|
||||
"autolinking": {
|
||||
@@ -157,7 +159,6 @@
|
||||
},
|
||||
"install": {
|
||||
"exclude": [
|
||||
"react-native@~0.76.6",
|
||||
"react-native-reanimated@~3.16.1",
|
||||
"react-native-gesture-handler@~2.20.0",
|
||||
"react-native-screens@~4.4.0",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
||||
|
||||
shim.setReact(React);
|
||||
PerformanceLogger.onAppStartBegin();
|
||||
|
||||
import setupQuickActions from './setupQuickActions';
|
||||
@@ -38,13 +35,14 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import NotesScreen from './components/screens/Notes/Notes';
|
||||
import TagsScreen from './components/screens/tags';
|
||||
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
import FolderScreen from './components/screens/folder';
|
||||
import LogScreen from './components/screens/LogScreen';
|
||||
import StatusScreen from './components/screens/status';
|
||||
import SearchScreen from './components/screens/SearchScreen';
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||
import DropboxLoginScreen from './components/screens/dropbox-login.js';
|
||||
import WebDavOidcLoginScreen from './components/screens/webdav-oidc-login';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import SideMenu, { SideMenuPosition } from './components/SideMenu';
|
||||
import SideMenuContent from './components/side-menu-content';
|
||||
@@ -56,8 +54,8 @@ import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import { themeStyle } from './components/global-style';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import SyncTargetFilesystem from '@joplin/lib/SyncTargetFilesystem';
|
||||
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
|
||||
import SyncTargetNextcloud from '@joplin/lib/SyncTargetNextcloud';
|
||||
import SyncTargetWebDAV from '@joplin/lib/SyncTargetWebDAV';
|
||||
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
|
||||
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
||||
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
|
||||
@@ -445,6 +443,10 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
state: 'ready',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
perfLogger.mark('Application is ready');
|
||||
}, 50);
|
||||
|
||||
// setTimeout(() => {
|
||||
// this.props.dispatch({
|
||||
// type: 'NAV_GO',
|
||||
@@ -695,12 +697,12 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
let disableSideMenuGestures = this.props.disableSideMenuGestures;
|
||||
|
||||
if (this.props.routeName === 'Note') {
|
||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
||||
sideMenuContent = <SideMenuContentNote options={this.props.noteSideMenuOptions}/>;
|
||||
menuPosition = SideMenuPosition.Right;
|
||||
} else if (this.props.routeName === 'Config') {
|
||||
disableSideMenuGestures = true;
|
||||
} else {
|
||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
|
||||
sideMenuContent = <SideMenuContent/>;
|
||||
}
|
||||
|
||||
const appNavInit = {
|
||||
@@ -711,6 +713,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||
DropboxLogin: { screen: DropboxLoginScreen },
|
||||
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
|
||||
WebDavOidcLogin: { screen: WebDavOidcLoginScreen },
|
||||
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
|
||||
|
||||
@@ -71,7 +71,10 @@ const crypto: Crypto = {
|
||||
|
||||
digest: async (algorithm: Digest, data: Uint8Array) => {
|
||||
const hash = QuickCrypto.createHash(digestNameMap[algorithm]);
|
||||
hash.update(data);
|
||||
hash.update(
|
||||
// Cast: hash.update accepts TypedArrays, despite its declared types
|
||||
data as unknown as ArrayBuffer,
|
||||
);
|
||||
return hash.digest();
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user