Compare commits
160 Commits
ios-v13.5.
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
269260ac48 | ||
|
|
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 | ||
|
|
5b74e206ed | ||
|
|
9873d02b0b | ||
|
|
57b7d98d8a | ||
|
|
f075b561a2 | ||
|
|
483d051de0 | ||
|
|
106cd2778f | ||
|
|
c3aea2db80 | ||
|
|
3f067b0f77 | ||
|
|
15cf025bc2 | ||
|
|
4677586e3b | ||
|
|
b8c5b7a153 | ||
|
|
e46e634c2e | ||
|
|
b3cf4e5a35 | ||
|
|
8589e10d6e | ||
|
|
18942f0d6a | ||
|
|
3be354cdcb | ||
|
|
0575f1aa3e | ||
|
|
caa9baa460 | ||
|
|
b5284804d8 | ||
|
|
6053b4296c | ||
|
|
615fec1d2c | ||
|
|
0bbcd9a59b | ||
|
|
6931b32f17 | ||
|
|
17ac501ddb | ||
|
|
94161c5f93 | ||
|
|
196255e960 | ||
|
|
f936390ee4 | ||
|
|
5638c4b812 | ||
|
|
4222caa423 | ||
|
|
bc705acc5c | ||
|
|
f1c968c19a | ||
|
|
26c5a6181e | ||
|
|
a3bf0cfdeb | ||
|
|
606b397326 | ||
|
|
fbd157283d | ||
|
|
2e879f65fc | ||
|
|
c727156a46 | ||
|
|
4e31f1918d | ||
|
|
a1cdf67779 | ||
|
|
5cb1db197f | ||
|
|
05c3065c72 | ||
|
|
25a5be09bf | ||
|
|
f0a3f73ddb | ||
|
|
1bb5d9ade5 | ||
|
|
e75875c1b0 | ||
|
|
cce4b76e3f | ||
|
|
b310bfd0c2 | ||
|
|
e19e1ac040 | ||
|
|
3bba2f6b2a | ||
|
|
ca9addcda0 | ||
|
|
c42a49c1cf | ||
|
|
a1e056670d | ||
|
|
6d7a70c21a | ||
|
|
14fd3c66c1 | ||
|
|
376f44a0ce | ||
|
|
4d81ee4c7f |
@@ -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
|
||||
@@ -468,6 +469,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 +514,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 +695,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,6 +874,7 @@ 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/side-menu-content.js
|
||||
@@ -969,6 +975,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 +1052,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 +1070,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 +1112,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
|
||||
@@ -1704,6 +1716,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
|
||||
@@ -1805,8 +1818,11 @@ packages/renderer/MdToHtml/renderMedia.js
|
||||
packages/renderer/MdToHtml/rules/abc.js
|
||||
packages/renderer/MdToHtml/rules/checkbox.js
|
||||
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
|
||||
@@ -1840,22 +1856,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
|
||||
@@ -1927,4 +1950,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
|
||||
|
||||
42
.github/workflows/build-macos-m1.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
# See github-action-main.yml for explanation
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Set Publish Flag
|
||||
run: |
|
||||
@@ -48,6 +48,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
|
||||
@@ -57,25 +58,38 @@ jobs:
|
||||
yarn install
|
||||
cd packages/app-desktop
|
||||
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
|
||||
|
||||
npm pkg delete 'build.mac.target'
|
||||
npm pkg set 'build.mac.target[0].target'='dmg'
|
||||
npm pkg set 'build.mac.target[0].arch[0]'='arm64'
|
||||
npm pkg set 'build.mac.target[1].target'='zip'
|
||||
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
|
||||
|
||||
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
|
||||
# Only enable pkg build in the main repository CI. As of 01/15/2026, pkg
|
||||
# build fails when running on external pull requests.
|
||||
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
|
||||
npm pkg set 'build.mac.target[2].target'='pkg'
|
||||
npm pkg set 'build.mac.target[2].arch[0]'='arm64'
|
||||
fi
|
||||
|
||||
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
build_dist() {
|
||||
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
|
||||
|
||||
# We also want to disable signing the app in this case, because
|
||||
# it doesn't work and we don't need it.
|
||||
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
|
||||
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npm pkg set 'build.mac.identity'=null --json
|
||||
# We also want to disable signing the app in this case, because
|
||||
# it doesn't work and we don't need it.
|
||||
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
|
||||
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
|
||||
fi
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npm pkg set 'build.mac.identity'=null --json
|
||||
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
|
||||
fi
|
||||
}
|
||||
|
||||
build_dist || build_dist
|
||||
@@ -72,4 +72,4 @@ runs:
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
|
||||
27
.gitignore
vendored
@@ -53,6 +53,7 @@ lerna-debug.log
|
||||
docs/**/*.mustache
|
||||
.idea
|
||||
/readme/i18n
|
||||
.watchman-cookie-*
|
||||
|
||||
# Yarn stuff
|
||||
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||
@@ -441,6 +442,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
|
||||
@@ -484,6 +487,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
|
||||
@@ -664,6 +668,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
|
||||
@@ -842,6 +847,7 @@ 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/side-menu-content.js
|
||||
@@ -942,6 +948,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
|
||||
@@ -1018,6 +1025,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
|
||||
@@ -1034,6 +1043,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
|
||||
@@ -1074,6 +1085,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
|
||||
@@ -1677,6 +1689,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
|
||||
@@ -1778,8 +1791,11 @@ packages/renderer/MdToHtml/renderMedia.js
|
||||
packages/renderer/MdToHtml/rules/abc.js
|
||||
packages/renderer/MdToHtml/rules/checkbox.js
|
||||
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
|
||||
@@ -1813,22 +1829,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
|
||||
@@ -1900,5 +1923,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 {
|
||||
BIN
Assets/WebsiteAssets/images/news/20260111-abc.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-lowercase-tags.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-mobile-tags.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-multi-select.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-profiles.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-rte1.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-rte2.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
@@ -1,4 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 22 Sep 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Sun, 11 Jan 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.5]]></title><description><![CDATA[<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>More stable and consistent Markdown editing<a name="more-stable-and-consistent-markdown-editing" href="#more-stable-and-consistent-markdown-editing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Markdown editor has been refined to feel more stable and closer to the final rendered view. Headings in the editor now more closely match how they appear when viewing a note, reducing the visual jump between editing and reading. Layout issues have also been addressed so elements like rendered checkboxes and images no longer cause the editor to shift unexpectedly while typing.</p>
|
||||
<p>The ABC music notation plugin appeared to be popular but had some limitations. With this new version, ABC is now part of the app, which means it can now work from published notes, and from the Rich Text editor!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-abc.png" alt="ABC music notation rendered directly in Joplin, showing a short musical phrase displayed from plain-text ABC syntax"></p>
|
||||
<h3>Smoother switching between notes<a name="smoother-switching-between-notes" href="#smoother-switching-between-notes" class="heading-anchor">🔗</a></h3>
|
||||
<p>Switching between notes is now less disruptive. Joplin restores cursor position and scroll location more reliably, making it easier to move back and forth between notes—especially when working with longer documents or comparing content—without losing your place.</p>
|
||||
<h3>Case insensitive tags<a name="case-insensitive-tags" href="#case-insensitive-tags" class="heading-anchor">🔗</a></h3>
|
||||
<p>Tags are now treated in a case-insensitive way, which helps prevent duplicate tags caused by differences in capitalisation, while still allowing mixed-case tag names. All this time we were hoping that @dpoulton <a href="https://discourse.joplinapp.org/t/tags-lower-case-only/4220/106">would just get used to lowercase tags</a>, but 5 years later it looks like it's not happening ;) So thank you @mrjo118 for implementing it!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-lowercase-tags.png" alt="Joplin tag list demonstrating case-insensitive tags, with mixed-case tag names merged into a single tag."></p>
|
||||
<h3>More reliable syncing and sharing<a name="more-reliable-syncing-and-sharing" href="#more-reliable-syncing-and-sharing" class="heading-anchor">🔗</a></h3>
|
||||
<p>Syncing and sharing have been made more robust in everyday use. Joplin now handles repeated syncs more efficiently, avoids unnecessary data usage, and is better at detecting and syncing all changes, particularly when using WebDAV and S3 sync targets.</p>
|
||||
<p>Moreover filesystem synchronisation is now more reliable, in particular when used alongside tools like SyncThing on both mobile and desktop.</p>
|
||||
<h3>Accessibility and readability improvements<a name="accessibility-and-readability-improvements" href="#accessibility-and-readability-improvements" class="heading-anchor">🔗</a></h3>
|
||||
<p>Accessibility has seen further refinements in this release. Dark mode readability has been improved, common editor elements are clearer, and animations are reduced or disabled when system “reduce motion” settings are enabled, making the app more comfortable to use for a wider range of users. Keyboard navigation has also been improved on the desktop application.</p>
|
||||
<h2>Desktop-specific improvements<a name="desktop-specific-improvements" href="#desktop-specific-improvements" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Easier profile management<a name="easier-profile-management" href="#easier-profile-management" class="heading-anchor">🔗</a></h3>
|
||||
<p>Managing multiple profiles on desktop is now simpler thanks to a new, more user-friendly profile management interface. This removes the need to manually edit configuration files and makes switching between different setups easier and safer.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-profiles.png" alt="Desktop profile management screen in Joplin showing multiple profiles with options to rename or delete them."></p>
|
||||
<h3>Significantly improved OneNote import<a name="significantly-improved-onenote-import" href="#significantly-improved-onenote-import" class="heading-anchor">🔗</a></h3>
|
||||
<p>Importing content from OneNote is now more reliable and accurate. Support has been expanded to cover more OneNote file formats, and many edge cases have been addressed so imported notes more closely match their original structure and content. This makes migrating from OneNote to Joplin smoother and more trustworthy.</p>
|
||||
<h3>Better tools for organising large note collections<a name="better-tools-for-organising-large-note-collections" href="#better-tools-for-organising-large-note-collections" class="heading-anchor">🔗</a></h3>
|
||||
<p>Desktop users can now select multiple notebooks at once, making it easier to reorganise notebook structures, move groups of notes, or clean up larger collections without working notebook by notebook.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-multi-select.png" alt="Joplin desktop sidebar with several notebooks selected at the same time for bulk organisation."></p>
|
||||
<h3>Polished editing experience on desktop<a name="polished-editing-experience-on-desktop" href="#polished-editing-experience-on-desktop" class="heading-anchor">🔗</a></h3>
|
||||
<p>Both the Markdown and Rich Text editors have been further refined. Cursor behaviour is more predictable, visual consistency between editing and viewing has improved, and several layout and rendering issues have been fixed to reduce interruptions while writing.</p>
|
||||
<h3>More reliable search and navigation<a name="more-reliable-search-and-navigation" href="#more-reliable-search-and-navigation" class="heading-anchor">🔗</a></h3>
|
||||
<p>Search and navigation on desktop have been improved with fixes that ensure search results behave consistently and remain visible when moving between windows or views.</p>
|
||||
<h3>Improved math support in WebClipper<a name="improved-math-support-in-webclipper" href="#improved-math-support-in-webclipper" class="heading-anchor">🔗</a></h3>
|
||||
<p>The WebClipper is not forgotten in this release - clipping certain math formulas, in particular from Wikipedia but also other websites, has been improved. Additionally, certain scientific articles are now also better handled by the WebClipper.</p>
|
||||
<h2>Mobile-specific improvements<a name="mobile-specific-improvements" href="#mobile-specific-improvements" class="heading-anchor">🔗</a></h2>
|
||||
<h3>A more powerful Rich Text Editor on mobile<a name="a-more-powerful-rich-text-editor-on-mobile" href="#a-more-powerful-rich-text-editor-on-mobile" class="heading-anchor">🔗</a></h3>
|
||||
<p>The mobile Rich Text Editor continues to improve, with new and expanded support for tables, code blocks, and other structured content. These changes make it easier to create and edit more complex notes directly on mobile devices.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-rte1.png" alt="Joplin mobile Rich Text Editor showing table editing controls and an embedded code block inside a note."></p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-rte2.png" alt="Mobile code block editor in Joplin with a Python code snippet displayed in an editable dialog."></p>
|
||||
<h3>Easier tag management on mobile<a name="easier-tag-management-on-mobile" href="#easier-tag-management-on-mobile" class="heading-anchor">🔗</a></h3>
|
||||
<p>Managing tags on mobile is now more practical. You can rename and delete tags directly from the app, and searching through tags is easier, helping keep large tag lists organised over time.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-mobile-tags.png" alt="Joplin mobile tag management screen showing a tag options menu with rename and delete actions."></p>
|
||||
<h3>Improved stability and usability on mobile devices<a name="improved-stability-and-usability-on-mobile-devices" href="#improved-stability-and-usability-on-mobile-devices" class="heading-anchor">🔗</a></h3>
|
||||
<p>Several fixes improve overall stability and usability on mobile, particularly on smaller screens. Issues causing UI elements to appear off-screen have been addressed, and the app behaves more consistently in situations that previously caused hangs or visual glitches.</p>
|
||||
<h2>Bug fixes and security fixes across platforms<a name="bug-fixes-and-security-fixes-across-platforms" href="#bug-fixes-and-security-fixes-across-platforms" class="heading-anchor">🔗</a></h2>
|
||||
<h3>A large number of stability, correctness and security fixes<a name="a-large-number-of-stability-correctness-and-security-fixes" href="#a-large-number-of-stability-correctness-and-security-fixes" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin 3.5 includes about 114 bug fixes across desktop and mobile, addressing issues in editing, syncing, importing, rendering, and general stability. Many fixes target edge cases that could lead to crashes, inconsistent behaviour, or rare data loss scenarios. Moreover, this version includes several vulnerability fixes to make the applications more secure.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20260111-release-3-5</link><guid isPermaLink="false">20260111-release-3-5</guid><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
|
||||
<h2>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Rich Text Editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
|
||||
<p>The mobile app now includes a beta <a href="https://joplinapp.org/help/apps/rich_text_editor">Rich Text Editor</a>! The new editor renders formatting/math/images within the editor:</p>
|
||||
@@ -481,42 +524,4 @@ sys 0m38.013s</p>
|
||||
<p>This is a bit of an extra constraint but it is hard to avoid. Contributor License Agreements are very common for GPL or AGPL projects. For example Apache, Canonical or Python all require their contributors to sign a CLA.</p>
|
||||
<h2>Questions?<a name="questions" href="#questions" class="heading-anchor">🔗</a></h2>
|
||||
<p>If you have any questions please let us know. Overall we believe this is a positive improvements for Joplin as it means any work derives from it will also benefit the project.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.9]]></title><description><![CDATA[<h2>Proxy support<a name="proxy-support" href="#proxy-support" class="heading-anchor">🔗</a></h2>
|
||||
<p>Both the desktop and mobile application now support proxies thanks to the work of Jason Williams. This will allow you to use the apps in particular when you are behind a company proxy.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-proxy-support.png" alt=""></p>
|
||||
<h2>New PDF viewer<a name="new-pdf-viewer" href="#new-pdf-viewer" class="heading-anchor">🔗</a></h2>
|
||||
<p>The desktop application now features a new PDF viewer thanks to the work of Asrient during GSoC.</p>
|
||||
<p>The main advantage for now is that this viewer preserves the last PDF page that was read. In the next version, the viewer will also include a way to annotate PDF files.</p>
|
||||
<h2>Multi-language spell checking<a name="multi-language-spell-checking" href="#multi-language-spell-checking" class="heading-anchor">🔗</a></h2>
|
||||
<p>The desktop app include a multi-language spell checking features, which allows you, for example, to spell-check notes in your native language and in English.</p>
|
||||
<h2>New mobile text editor<a name="new-mobile-text-editor" href="#new-mobile-text-editor" class="heading-anchor">🔗</a></h2>
|
||||
<p>Writing formatted notes on mobile has always been cumbersome due to the need to enter special format characters like <code>*</code> or <code>[</code>, etc.</p>
|
||||
<p>Thanks to the work of Henry Heino during GSoC, writing notes on the go is now easier thanks to an improved Markdown editor.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-mobile-beta-editor.png" alt=""></p>
|
||||
<p>The most visible feature is the addition of a toolbar, which helps input those special characters, like on desktop.</p>
|
||||
<p>Moreover Henry made a lot of subtle but useful improvements to the editor, for example to improve the note appearance, to improve list continuation, etc. Search within a note is now also supported as well as spell-checking.</p>
|
||||
<p>At a more technical level, Henry also added many test units to ensure that the editor remains robust and reliable.</p>
|
||||
<p>To enable the feature, go to the configuration screen and selected "Opt-in to the editor beta". It is already very stable so we will probably promote it to be the main editor from the next version.</p>
|
||||
<h2>Improved alignment of notebook icons<a name="improved-alignment-of-notebook-icons" href="#improved-alignment-of-notebook-icons" class="heading-anchor">🔗</a></h2>
|
||||
<p>Previously, when you would assign an icon to a notebook, it would shift the title to the right, but notebook without an icon would not. It means that notebooks with and without an icon would not be vertically aligned.</p>
|
||||
<p>To tidy things up, this new version adds a default icons to notebooks without an explicitly assigned icon. This result in the notebook titles being correctly vertically aligned.</p>
|
||||
<p>Note that this feature is only enabled if you use custom icons - otherwise it will simply display the notebook titles without any default icons, as before.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-notebook-icons.png" alt=""></p>
|
||||
<h2>Improved handling of file attachments<a name="improved-handling-of-file-attachments" href="#improved-handling-of-file-attachments" class="heading-anchor">🔗</a></h2>
|
||||
<p>Self Not Found made a number of small but useful improvements to attachment handling, including increasing the maximum size to 200MB, adding support for attaching multiple files, and fixing issues with synchronising attachments via proxy.</p>
|
||||
<h2>Fixed filesystem sync on mobile<a name="fixed-filesystem-sync-on-mobile" href="#fixed-filesystem-sync-on-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<p>This was a long and complex change due to the need to support new Android APIs but hopefully that should now be working again, thanks to the work of jd1378.</p>
|
||||
<p>So you can now sync again your notes with Syncthing and other file-based synchronisation systems.</p>
|
||||
<h2>And more...<a name="and-more" href="#and-more" class="heading-anchor">🔗</a></h2>
|
||||
<p>In total this new desktop version includes 36 improvements, bug fixes, and security fixes.</p>
|
||||
<p>As always, a lot of work went into the Android and iOS app too, which include 37 improvements, bug fixes, and security fixes.</p>
|
||||
<p>See here for the changelogs:</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/help/about/changelog/desktop">Desktop app changelog</a></li>
|
||||
<li><a href="https://joplinapp.org/help/about/changelog/android/">Android app changelog</a></li>
|
||||
</ul>
|
||||
<h2>About the Android version<a name="about-the-android-version" href="#about-the-android-version" class="heading-anchor">🔗</a></h2>
|
||||
<p>Unfortunately we cannot publish the Android version because it is based on a framework version that Google does not accept. To upgrade the app a lot of changes are needed and another round of pre-releases, and therefore there will not be a 2.9 version for Google Play. You may however download the official APK directly from there: <a href="https://github.com/laurent22/joplin-android/releases/tag/android-v2.9.8">Android 2.9 Official Release</a></p>
|
||||
<p>This is the reality of app stores in general - small developers being imposed never ending new requirements by all-powerful companies, and by the time a version is finally ready we can't even publish it because yet more requirements are in place.</p>
|
||||
<p>For the record the current 2.9 app works perfectly fine. It targets Android 11, which is only 2 years old and is still supported (and installed on millions of phones). Google requires us to target Android 12 which only came out last year.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.9</twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item></channel></rss>
|
||||
@@ -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",
|
||||
|
||||
6
jest.config.base.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// This is the base Jest configuration - all
|
||||
// jest.config.js files should inherit from it.
|
||||
|
||||
module.exports = {
|
||||
watchman: false,
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
"./packages/app-cli/**/*.mo": true,
|
||||
"./packages/app-cli/**/build/": true,
|
||||
"./packages/app-cli/**/config.json": true,
|
||||
"**/.watchman-cookie-*": true,
|
||||
"./packages/app-cli/**/linkToLocal.sh": true,
|
||||
"./packages/app-cli/**/node_modules/": true,
|
||||
"./packages/app-cli/**/out.txt": true,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -21,7 +21,8 @@ class Command extends BaseCommand {
|
||||
public override async action(_args: Args) {
|
||||
const keymaps = await app().loadKeymaps();
|
||||
|
||||
this.stdout(_('Configured keyboard shortcuts:\n'));
|
||||
this.stdout(_('Configured keyboard shortcuts:'));
|
||||
this.stdout('\n');
|
||||
|
||||
const rows = [];
|
||||
const padding = ' ';
|
||||
@@ -31,7 +32,7 @@ class Command extends BaseCommand {
|
||||
|
||||
for (const item of keymaps) {
|
||||
const formattedKeys = item.keys
|
||||
.map((k: string) => (k === ' ' ? `(${'SPACE'})` : k))
|
||||
.map((k: string) => (k === ' ' ? `(${_('SPACE')})` : k))
|
||||
.join(', ');
|
||||
rows.push([padding + formattedKeys, item.type, item.command]);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
// 4. Remove tests one by one to narrow it down to the one with the async
|
||||
// call that's causing problem.
|
||||
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
testMatch: [
|
||||
'**/tests/HtmlToHtml.js',
|
||||
'**/tests/HtmlToMd.js',
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -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",
|
||||
@@ -70,7 +70,7 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.130",
|
||||
|
||||
@@ -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!
|
||||
|
||||
```
|
||||
8
packages/app-cli/tests/md_to_html/external_embed.html
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
<div class="joplin-editable">
|
||||
<span class="joplin-source" data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<div class="joplin-youtube-player-rendered">
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/iJqe9pC-z-Y" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
packages/app-cli/tests/md_to_html/external_embed.md
Normal file
@@ -0,0 +1 @@
|
||||
https://www.youtube.com/watch?v=iJqe9pC-z-Y
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
|
||||
@@ -260,6 +260,15 @@ export default class ElectronAppWrapper {
|
||||
|
||||
require('@electron/remote/main').enable(this.win_.webContents);
|
||||
|
||||
// Add Referer header for YouTube embeds to fix Error 153
|
||||
this.win_.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
{ urls: ['*://*.youtube.com/*', '*://*.youtube-nocookie.com/*'] },
|
||||
(details, callback) => {
|
||||
details.requestHeaders['Referer'] = 'https://joplinapp.org/';
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
},
|
||||
);
|
||||
|
||||
if (!screen.getDisplayMatching(this.win_.getBounds())) {
|
||||
const { width: windowWidth, height: windowHeight } = this.win_.getBounds();
|
||||
const { width: primaryDisplayWidth, height: primaryDisplayHeight } = screen.getPrimaryDisplay().workArea;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]) {
|
||||
@@ -742,7 +749,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
'media-src \'self\' blob: data: *', // Audio and video players
|
||||
|
||||
// Disallow certain unused features
|
||||
'child-src \'none\'', // Should not contain sub-frames
|
||||
'child-src https://*.youtube.com https://*.youtube-nocookie.com', // Allow YouTube embeds
|
||||
'object-src \'none\'', // Objects can be used for script injection
|
||||
'form-action \'none\'', // No submitting forms
|
||||
|
||||
|
||||
@@ -22,4 +22,8 @@ export const joplinCommandToTinyMceCommands: JoplinCommandToTinyMceCommands = {
|
||||
'search': { name: 'SearchReplace' },
|
||||
'attachFile': { name: 'joplinAttach' },
|
||||
'insertDateTime': true,
|
||||
'textCopy': true,
|
||||
'textCut': true,
|
||||
'textPaste': true,
|
||||
'textSelectAll': true,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,19 +17,19 @@ describe('editorCommandDeclarations', () => {
|
||||
test.each([
|
||||
[
|
||||
{},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
{
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, but only the viewer is visible
|
||||
@@ -37,7 +37,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, and the viewer is visible
|
||||
@@ -45,7 +45,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: true,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// In the RT editor
|
||||
@@ -53,7 +53,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, and the command palette is visible
|
||||
@@ -63,14 +63,57 @@ describe('editorCommandDeclarations', () => {
|
||||
gotoAnythingVisible: true,
|
||||
modalDialogVisible: true,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
])('should create the enabledCondition', (context: Record<string, any>, expected: boolean) => {
|
||||
const condition = enabledCondition('textBold');
|
||||
const wc = new WhenClause(condition);
|
||||
const actual = wc.evaluate({ ...baseContext, ...context });
|
||||
expect(actual).toBe(expected);
|
||||
[
|
||||
// In the Markdown editor, and the command palette is visible
|
||||
{
|
||||
markdownEditorPaneVisible: true,
|
||||
richTextEditorVisible: false,
|
||||
gotoAnythingVisible: true,
|
||||
modalDialogVisible: true,
|
||||
},
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// Rich Text Editor, HTML note
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
noteIsMarkdown: false,
|
||||
},
|
||||
{
|
||||
textCopy: true,
|
||||
textPaste: true,
|
||||
textSelectAll: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
// Rich Text Editor, read-only note
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
{
|
||||
textBold: false,
|
||||
textPaste: false,
|
||||
|
||||
// TODO: textCopy should be enabled in read-only notes:
|
||||
// textCopy: false,
|
||||
},
|
||||
],
|
||||
])('should correctly determine whether command is enabled (case %#)', (context, expectedStates) => {
|
||||
const actualStates = [];
|
||||
for (const commandName of Object.keys(expectedStates)) {
|
||||
const condition = enabledCondition(commandName);
|
||||
const wc = new WhenClause(condition);
|
||||
const actual = wc.evaluate({ ...baseContext, ...context });
|
||||
actualStates.push([commandName, actual]);
|
||||
}
|
||||
|
||||
const expectedStatesArray = Object.entries(expectedStates);
|
||||
expect(actualStates).toEqual(expectedStatesArray);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,6 +4,10 @@ import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinC
|
||||
|
||||
const workWithHtmlNotes = [
|
||||
'attachFile',
|
||||
'textCopy',
|
||||
'textCut',
|
||||
'textPaste',
|
||||
'textSelectAll',
|
||||
];
|
||||
|
||||
export const enabledCondition = (commandName: string) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
};
|
||||
@@ -124,8 +124,7 @@ export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
|
||||
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onError: (error: any) => {
|
||||
onError: (error: string|Error) => {
|
||||
errors.push(error);
|
||||
console.warn(error);
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
default-src 'self' joplin-content://* ;
|
||||
connect-src 'self' * http://* https://* joplin-content://* blob: ;
|
||||
style-src 'unsafe-inline' 'self' blob: joplin-content://* https://* http://* ;
|
||||
child-src 'self' joplin-content://* ;
|
||||
child-src 'self' joplin-content://* https://*.youtube.com https://*.youtube-nocookie.com ;
|
||||
script-src 'self' 'unsafe-inline' joplin-content://* ;
|
||||
media-src 'self' * blob: data: https://* http://* joplin-content://* ;
|
||||
img-src 'self' blob: data: http://* https://* joplin-content://* ;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
@@ -128,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.5.10",
|
||||
"version": "3.6.2",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -92,6 +92,12 @@
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pkg",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
@@ -139,19 +145,19 @@
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.5",
|
||||
"@joplin/editor": "~3.5",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/default-plugins": "~3.6",
|
||||
"@joplin/editor": "~3.6",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.25",
|
||||
"@types/react": "18.3.26",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
@@ -163,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",
|
||||
@@ -179,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",
|
||||
@@ -208,7 +214,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"@joplin/onenote-converter": "~3.6",
|
||||
"fs-extra": "11.3.2",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
|
||||
@@ -140,7 +140,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
|
||||
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
||||
await autoUpdater.checkForUpdates();
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
|
||||
// Wait for the installation to finish. By default, .checkForUpdates runs in the background
|
||||
await result.downloadPromise;
|
||||
} catch (error) {
|
||||
this.logger_.error(`Update download url failed: ${error.message}`);
|
||||
this.isUpdateInProgress = false;
|
||||
|
||||
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 2097787
|
||||
versionName "3.5.7"
|
||||
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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
|
||||