Compare commits
189 Commits
server-v3.
...
plugin-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1aec4a9f7 | ||
|
|
cab1525589 | ||
|
|
a52f3fea9e | ||
|
|
dfbd5eb8ed | ||
|
|
3131f36033 | ||
|
|
dc5b2cfa21 | ||
|
|
cad0f35fcc | ||
|
|
38ea92ff57 | ||
|
|
830deada22 | ||
|
|
38cd4033ea | ||
|
|
02900752d9 | ||
|
|
091e9813b5 | ||
|
|
e61e5ac32a | ||
|
|
414970c9a1 | ||
|
|
d4ed49ff23 | ||
|
|
8751d5d152 | ||
|
|
2e846fe15d | ||
|
|
e54b7696d9 | ||
|
|
553c61d628 | ||
|
|
6a15db3a36 | ||
|
|
6f1d0a4b90 | ||
|
|
33b995672c | ||
|
|
8ee46bb4e7 | ||
|
|
b35d9a64cf | ||
|
|
64ef74dd01 | ||
|
|
53035839a5 | ||
|
|
af5287de99 | ||
|
|
45a7554774 | ||
|
|
b06ffe3d25 | ||
|
|
53ea51b758 | ||
|
|
820acdc1f0 | ||
|
|
ef0a79666e | ||
|
|
d096a90c0e | ||
|
|
191775310e | ||
|
|
4fc351b861 | ||
|
|
396decd26c | ||
|
|
01f8fa7bef | ||
|
|
c40856ac7e | ||
|
|
d869cce413 | ||
|
|
a83e8311d8 | ||
|
|
aa884fcb39 | ||
|
|
be2a4c3e24 | ||
|
|
520eec555b | ||
|
|
1281fdb9d2 | ||
|
|
6029353fd1 | ||
|
|
8d1d1be79e | ||
|
|
fd180ae0b4 | ||
|
|
6fdfd6eae6 | ||
|
|
cd5bb575c8 | ||
|
|
2df56530ae | ||
|
|
7987137470 | ||
|
|
a1dcd2fd8f | ||
|
|
7826dc064a | ||
|
|
eedf083bfd | ||
|
|
d4aa1f8f8d | ||
|
|
738e749d51 | ||
|
|
8fe818c0b0 | ||
|
|
e603452fad | ||
|
|
3827637b54 | ||
|
|
1da7c54e5f | ||
|
|
e24ebffba6 | ||
|
|
e5bd77836a | ||
|
|
8f5e628303 | ||
|
|
6850c8128b | ||
|
|
8a797fdf23 | ||
|
|
1ae550c0aa | ||
|
|
e7e0529f52 | ||
|
|
2381e44c7f | ||
|
|
a59e975f73 | ||
|
|
2d703b6292 | ||
|
|
b8db70f707 | ||
|
|
c91513b6b5 | ||
|
|
a57ada97ef | ||
|
|
d8677a70dd | ||
|
|
15839a19fd | ||
|
|
8f1d55c1fc | ||
|
|
98c18711f7 | ||
|
|
24ff4612fb | ||
|
|
f832eb38ff | ||
|
|
91dc23c23f | ||
|
|
d1913493ab | ||
|
|
fd2b22ed68 | ||
|
|
14b56f19df | ||
|
|
0b082a985b | ||
|
|
53dcac22d0 | ||
|
|
2c721a76b7 | ||
|
|
b68cfd6d9e | ||
|
|
affebedc4b | ||
|
|
a714ef4807 | ||
|
|
596f99aad3 | ||
|
|
c530d35b36 | ||
|
|
5a5c734e2a | ||
|
|
f7eb483d9a | ||
|
|
7f3c7e807c | ||
|
|
a50fc02b32 | ||
|
|
63702e9e34 | ||
|
|
92c67aab4e | ||
|
|
91535870a2 | ||
|
|
d4bb277417 | ||
|
|
90f87d1496 | ||
|
|
b07752b3ab | ||
|
|
98effef4c5 | ||
|
|
32a919eb81 | ||
|
|
e124fd5c9f | ||
|
|
c5f9290402 | ||
|
|
c80cdadc99 | ||
|
|
d96dcef109 | ||
|
|
33b889ca38 | ||
|
|
fa78ea0173 | ||
|
|
6705712f80 | ||
|
|
2785b7f7d9 | ||
|
|
f04831406e | ||
|
|
fdffc81834 | ||
|
|
6f113df2d6 | ||
|
|
8b8b6fbe36 | ||
|
|
1ef8fd529b | ||
|
|
9547a459cb | ||
|
|
be1d092cab | ||
|
|
517669ee27 | ||
|
|
72fc97116f | ||
|
|
77ca6b3447 | ||
|
|
b227d337d0 | ||
|
|
a6e671d45b | ||
|
|
47c82a7e75 | ||
|
|
bafa1576f2 | ||
|
|
48956df439 | ||
|
|
4716065295 | ||
|
|
f801bbfb27 | ||
|
|
4a043f68ad | ||
|
|
cac93e9f9c | ||
|
|
e1e5c9aeb0 | ||
|
|
382cb257ab | ||
|
|
6f375be8b9 | ||
|
|
a118615e06 | ||
|
|
912bf7463f | ||
|
|
cfc29832a2 | ||
|
|
737fd132e3 | ||
|
|
9fc76f4e4c | ||
|
|
981f15d85c | ||
|
|
a59594db3b | ||
|
|
8c8190e2e9 | ||
|
|
d7e7ff77e8 | ||
|
|
e33c142c5a | ||
|
|
97d3a8243d | ||
|
|
f1716a3edb | ||
|
|
1436f5867d | ||
|
|
d754b8fe0c | ||
|
|
4f58055cc1 | ||
|
|
98697e1db4 | ||
|
|
8ac65a08c1 | ||
|
|
2b86d83290 | ||
|
|
09cafe99d1 | ||
|
|
6fce844cbf | ||
|
|
52de8c071f | ||
|
|
537543cc8a | ||
|
|
ff16453299 | ||
|
|
210deec495 | ||
|
|
e96baea005 | ||
|
|
ae24b91f25 | ||
|
|
f2e5118bf5 | ||
|
|
72698ec573 | ||
|
|
68abc27c6a | ||
|
|
1acb3d0726 | ||
|
|
5bf97dc3b8 | ||
|
|
e0e04fbc91 | ||
|
|
625cd1221c | ||
|
|
110d5bde2d | ||
|
|
93a85b3207 | ||
|
|
ff305f42fd | ||
|
|
99ba854ee1 | ||
|
|
38b368e997 | ||
|
|
f9ffe6c4e6 | ||
|
|
5adc0170fc | ||
|
|
f54c364b4d | ||
|
|
9f541b9b9d | ||
|
|
bd0af08c57 | ||
|
|
ac06c6750d | ||
|
|
23b07094b7 | ||
|
|
7eefc016de | ||
|
|
c002be76cd | ||
|
|
2cd29aaaea | ||
|
|
4cb6b01c71 | ||
|
|
91c79b9488 | ||
|
|
fc516d05b3 | ||
|
|
2769c9586c | ||
|
|
fd15d5a6d3 | ||
|
|
7237d7faa7 | ||
|
|
3025d62568 | ||
|
|
5b5dcf34a1 |
@@ -9,6 +9,7 @@ API_KEY=random-string
|
||||
QUEUE_TTL=900000
|
||||
QUEUE_RETRY_COUNT=2
|
||||
QUEUE_MAINTENANCE_INTERVAL=30000
|
||||
IMAGE_MAX_DIMENSION=400
|
||||
|
||||
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:latest
|
||||
# Fullpath to images folder e.g.:
|
||||
|
||||
@@ -96,6 +96,7 @@ packages/onenote-converter/pkg/onenote_converter.js
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/cli-integration-tests.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
@@ -724,6 +725,8 @@ packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
|
||||
packages/app-mobile/components/SyncWizard/SyncWizard.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
@@ -740,6 +743,7 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/CardButton.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
@@ -840,6 +844,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -909,8 +914,6 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
@@ -1047,6 +1050,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1093,12 +1097,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
@@ -1110,12 +1117,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
@@ -1123,6 +1133,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
@@ -1133,6 +1144,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
@@ -1526,6 +1538,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1730,6 +1743,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1845,6 +1859,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
6
.github/workflows/build-android.yml
vendored
@@ -59,10 +59,10 @@ jobs:
|
||||
fi
|
||||
# The build-tools/ directory contains different subdirectories
|
||||
# for each build tools version. As a result, there may be multiple
|
||||
# zipalign tools. Select one of them:
|
||||
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | head -n1)"
|
||||
# zipalign tools. Select the most recent (biggest two-digit version number):
|
||||
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | sort | tail -n1)"
|
||||
if test ! -x "$ZIPALIGN_PATH" ; then
|
||||
echo "zipalign not found (searching in $BUILD_TOOLS_PATH, candidate: $ZIPALIGN_PATH)"
|
||||
exit 1
|
||||
fi
|
||||
"$ZIPALIGN_PATH" -c -P 16 -v 4 "$APK_FILE"
|
||||
"$ZIPALIGN_PATH" -c -P 16 -v 4 "$APK_FILE"
|
||||
|
||||
50
.github/workflows/github-actions-main.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-13, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -31,6 +31,16 @@ jobs:
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
|
||||
- name: Free disk space
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
sudo rm -rf /var/lib/docker/tmp/* || true
|
||||
|
||||
# Login to Docker only if we're on a server release tag. If we run this on
|
||||
# a pull request it will fail because the PR doesn't have access to
|
||||
# secrets
|
||||
@@ -40,6 +50,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Test Windows app signing
|
||||
# if: runner.os == 'Windows'
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# IS_CONTINUOUS_INTEGRATION: 1
|
||||
# BUILD_SEQUENCIAL: 1
|
||||
# SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
# SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
# SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
# SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
# SIGN_APPLICATION: 1
|
||||
# # To ensure that the operations stop on failure, all commands
|
||||
# # should be on one line with "&&" in between.
|
||||
# run: |
|
||||
# yarn install && cd packages/app-desktop && yarn dist
|
||||
|
||||
- name: Run tests, build and publish Linux and macOS apps
|
||||
if: runner.os == 'Linux' || runner.os == 'macOs'
|
||||
env:
|
||||
@@ -61,11 +87,14 @@ jobs:
|
||||
- name: Build and publish Windows app
|
||||
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
SIGN_APPLICATION: 1
|
||||
# To ensure that the operations stop on failure, all commands
|
||||
# should be on one line with "&&" in between.
|
||||
run: |
|
||||
@@ -122,6 +151,16 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Free disk space
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
sudo rm -rf /var/lib/docker/tmp/* || true
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
@@ -149,7 +188,7 @@ jobs:
|
||||
- name: Check HTTP request
|
||||
run: |
|
||||
# Need to pass environment variables:
|
||||
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
|
||||
# Wait for server to start
|
||||
sleep 120
|
||||
@@ -175,5 +214,4 @@ jobs:
|
||||
if [[ "$actual_body" != "$expected_body" ]]; then
|
||||
echo 'Failed while checking the body response after request to /api/ping'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
fi
|
||||
22
.gitignore
vendored
@@ -69,6 +69,7 @@ docs/**/*.mustache
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/cli-integration-tests.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
@@ -697,6 +698,8 @@ packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
|
||||
packages/app-mobile/components/SyncWizard/SyncWizard.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
@@ -713,6 +716,7 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/CardButton.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
@@ -813,6 +817,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -882,8 +887,6 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
@@ -1020,6 +1023,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1066,12 +1070,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
@@ -1083,12 +1090,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
@@ -1096,6 +1106,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
@@ -1106,6 +1117,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
@@ -1499,6 +1511,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1703,6 +1716,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1818,6 +1832,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
36
.yarn/patches/depd-npm-1.1.2-b0c8414da7.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
# Patch to remove eval. This allows using depd in an environment with
|
||||
# a strict Content-Security-Policy.
|
||||
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
|
||||
diff --git a/index.js b/index.js
|
||||
index d758d3c8f58a60bf27ef377ad77639bf10ce7854..2bad40d4eeba553d3bcfb206873eac059067ae3b 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -399,19 +399,20 @@ function wrapfunction (fn, message) {
|
||||
throw new TypeError('argument fn must be a function')
|
||||
}
|
||||
|
||||
- var args = createArgumentsString(fn.length)
|
||||
- var deprecate = this // eslint-disable-line no-unused-vars
|
||||
var stack = getStack()
|
||||
var site = callSiteLocation(stack[1])
|
||||
|
||||
site.name = fn.name
|
||||
|
||||
- // eslint-disable-next-line no-eval
|
||||
- var deprecatedfn = eval('(function (' + args + ') {\n' +
|
||||
- '"use strict"\n' +
|
||||
- 'log.call(deprecate, message, site)\n' +
|
||||
- 'return fn.apply(this, arguments)\n' +
|
||||
- '})')
|
||||
+ var deprecatedfn
|
||||
+ var self = this
|
||||
+ deprecatedfn = function () {
|
||||
+ 'use strict'
|
||||
+ log.call(self, message, site)
|
||||
+ return fn.apply(this, arguments)
|
||||
+ }
|
||||
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
|
||||
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
|
||||
|
||||
return deprecatedfn
|
||||
}
|
||||
35
.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch
Normal file
@@ -0,0 +1,35 @@
|
||||
# Patch to remove eval. This allows using depd in an environment with
|
||||
# a strict Content-Security-Policy.
|
||||
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
|
||||
diff --git a/index.js b/index.js
|
||||
index 1bf2fcfdeffc984e5ad792eec08744c29d4a4590..1b24aa2414458bc651abfdded81b103c131efeaa 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -415,19 +415,19 @@ function wrapfunction (fn, message) {
|
||||
throw new TypeError('argument fn must be a function')
|
||||
}
|
||||
|
||||
- var args = createArgumentsString(fn.length)
|
||||
var stack = getStack()
|
||||
var site = callSiteLocation(stack[1])
|
||||
|
||||
site.name = fn.name
|
||||
|
||||
- // eslint-disable-next-line no-new-func
|
||||
- var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
|
||||
- '"use strict"\n' +
|
||||
- 'return function (' + args + ') {' +
|
||||
- 'log.call(deprecate, message, site)\n' +
|
||||
- 'return fn.apply(this, arguments)\n' +
|
||||
- '}')(fn, log, this, message, site)
|
||||
+ var self = this
|
||||
+ var deprecatedfn = function () {
|
||||
+ 'use strict'
|
||||
+ log.call(self, message, site)
|
||||
+ return fn.apply(this, arguments)
|
||||
+ }
|
||||
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
|
||||
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
|
||||
|
||||
return deprecatedfn
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c95300854 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -67,19 +67,19 @@ repositories {
|
||||
}
|
||||
|
||||
// Generate UUIDs for each models contained in android/src/main/assets/
|
||||
-tasks.register('genUUID') {
|
||||
- doLast {
|
||||
- fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
- if (fileDetails.directory) {
|
||||
- def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
- def ofile = file("$odir/uuid")
|
||||
- mkdir odir
|
||||
- ofile.text = UUID.randomUUID().toString()
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-preBuild.dependsOn genUUID
|
||||
+// tasks.register('genUUID') {
|
||||
+// doLast {
|
||||
+// fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
+// if (fileDetails.directory) {
|
||||
+// def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
+// def ofile = file("$odir/uuid")
|
||||
+// mkdir odir
|
||||
+// ofile.text = UUID.randomUUID().toString()
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// preBuild.dependsOn genUUID
|
||||
|
||||
def kotlin_version = getExtOrDefault('kotlinVersion')
|
||||
|
||||
diff --git a/android/src/main/java/com/reactnativevosk/VoskModule.kt b/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..5a8539b9cce8951967640dba755e29a4e3ff404a 100644
|
||||
--- a/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
+++ b/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
@@ -19,13 +19,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
return "Vosk"
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun addListener(type: String?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun removeListeners(type: Int?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
override fun onResult(hypothesis: String) {
|
||||
// Get text data from string object
|
||||
val text = getHypothesisText(hypothesis)
|
||||
|
||||
// Stop recording if data found
|
||||
if (text != null && text.isNotEmpty()) {
|
||||
- cleanRecognizer();
|
||||
+ // Don't auto-stop the recogniser - we want to do that when the user
|
||||
+ // presses on "stop" only.
|
||||
+ // cleanRecognizer();
|
||||
sendEvent("onResult", text)
|
||||
}
|
||||
}
|
||||
@@ -93,12 +105,11 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
@ReactMethod
|
||||
fun loadModel(path: String, promise: Promise) {
|
||||
cleanModel();
|
||||
- StorageService.unpack(context, path, "models",
|
||||
- { model: Model? ->
|
||||
- this.model = model
|
||||
- promise.resolve("Model successfully loaded")
|
||||
- }
|
||||
- ) { e: IOException ->
|
||||
+
|
||||
+ try {
|
||||
+ this.model = Model(path);
|
||||
+ promise.resolve("Model successfully loaded")
|
||||
+ } catch (e: IOException) {
|
||||
this.model = null
|
||||
promise.reject(e)
|
||||
}
|
||||
@@ -153,6 +164,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
cleanRecognizer();
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun stopOnly() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.stop()
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun cleanup() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.shutdown();
|
||||
+ speechService = null
|
||||
+ }
|
||||
+ if (recognizer != null) {
|
||||
+ recognizer!!.close();
|
||||
+ recognizer = null;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@ReactMethod
|
||||
fun unload() {
|
||||
cleanRecognizer();
|
||||
diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts
|
||||
index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7c706e3c4 100644
|
||||
--- a/lib/typescript/index.d.ts
|
||||
+++ b/lib/typescript/index.d.ts
|
||||
@@ -10,6 +10,8 @@ export default class Vosk {
|
||||
currentRegisteredEvents: EmitterSubscription[];
|
||||
start: (grammar?: string[] | null) => Promise<String>;
|
||||
stop: () => void;
|
||||
+ stopOnly: () => void;
|
||||
+ cleanup: () => void;
|
||||
unload: () => void;
|
||||
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
diff --git a/package.json b/package.json
|
||||
index 707eddb8d68007f93071ac659c5b087c935c5f01..90ebe20f224eeec472c377df1fef9b15f2ff8200 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -11,12 +11,9 @@
|
||||
"src",
|
||||
"lib",
|
||||
"android",
|
||||
- "ios",
|
||||
"cpp",
|
||||
- "react-native-vosk.podspec",
|
||||
"!lib/typescript/example",
|
||||
"!android/build",
|
||||
- "!ios/build",
|
||||
"!**/__tests__",
|
||||
"!**/__fixtures__",
|
||||
"!**/__mocks__"
|
||||
diff --git a/react-native-vosk.podspec b/react-native-vosk.podspec
|
||||
deleted file mode 100644
|
||||
index e3d41b90c5eef890c7a5108aaf16ac07d34a698b..0000000000000000000000000000000000000000
|
||||
--- a/react-native-vosk.podspec
|
||||
+++ /dev/null
|
||||
@@ -1,41 +0,0 @@
|
||||
-require "json"
|
||||
-
|
||||
-package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||
-folly_version = '2021.06.28.00-v2'
|
||||
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
||||
-
|
||||
-Pod::Spec.new do |s|
|
||||
- s.name = "react-native-vosk"
|
||||
- s.version = package["version"]
|
||||
- s.summary = package["description"]
|
||||
- s.homepage = package["homepage"]
|
||||
- s.license = package["license"]
|
||||
- s.authors = package["author"]
|
||||
-
|
||||
- s.platforms = { :ios => "10.0" }
|
||||
- s.source = { :git => "https://github.com/riderodd/react-native-vosk.git", :tag => "#{s.version}" }
|
||||
-
|
||||
- s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
- s.resource_bundles = { 'Vosk' => ['ios/Vosk/*'] }
|
||||
-
|
||||
- s.dependency "React-Core"
|
||||
- s.frameworks = "Accelerate"
|
||||
- s.library = "c++"
|
||||
- s.vendored_frameworks = "ios/libvosk.xcframework"
|
||||
- s.requires_arc = true
|
||||
-
|
||||
- # Don't install the dependencies when we run `pod install` in the old architecture.
|
||||
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
||||
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
||||
- s.pod_target_xcconfig = {
|
||||
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
||||
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
||||
- }
|
||||
-
|
||||
- s.dependency "React-Codegen"
|
||||
- s.dependency "RCT-Folly", folly_version
|
||||
- s.dependency "RCTRequired"
|
||||
- s.dependency "RCTTypeSafety"
|
||||
- s.dependency "ReactCommon/turbomodule/core"
|
||||
- end
|
||||
-end
|
||||
diff --git a/src/index.tsx b/src/index.tsx
|
||||
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
|
||||
--- a/src/index.tsx
|
||||
+++ b/src/index.tsx
|
||||
@@ -69,6 +69,15 @@ export default class Vosk {
|
||||
VoskModule.stop();
|
||||
};
|
||||
|
||||
+ stopOnly = () => {
|
||||
+ VoskModule.stopOnly();
|
||||
+ };
|
||||
+
|
||||
+ cleanup = () => {
|
||||
+ this.cleanListeners();
|
||||
+ VoskModule.cleanup();
|
||||
+ };
|
||||
+
|
||||
unload = () => {
|
||||
this.cleanListeners();
|
||||
VoskModule.unload();
|
||||
BIN
Assets/WebsiteAssets/images/news/20250922-mobile-rte.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-note-history.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-publish-notes.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-scan-notebook.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-tag-editor.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/NotGamStop.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/WriteMyEssay.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
@@ -1,4 +1,77 @@
|
||||
<?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, 28 Apr 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 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>
|
||||
<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>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-mobile-rte.png" width="400" alt="screenshot: Mobile Rich Text Editor editing the welcome notes. Images, headings, etc are rendering."/>
|
||||
<p>To try it, 1) open a note in the default Markdown editor 2) open the note actions menu (the three vertical dots) for the note and 3) click “Edit as Rich Text”.</p>
|
||||
<p>Be aware that this editor is still in active development and <a href="https://github.com/laurent22/joplin/issues/12840">has a number of known limitations and issues</a>. The Rich Text editor is based on <a href="https://prosemirror.net/">ProseMirror</a> and will behave differently from the desktop Rich Text Editor in many cases.</p>
|
||||
<h3>Support for publishing notes with Joplin Cloud and Server<a name="support-for-publishing-notes-with-joplin-cloud-and-server" href="#support-for-publishing-notes-with-joplin-cloud-and-server" class="heading-anchor">🔗</a></h3>
|
||||
<p>It's now possible to <a href="https://joplinapp.org/help/apps/publish_note">publish notes</a> from the mobile app! To do so, open the “Properties” menu for a note, then click “Publish/unpublish”:</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-publish-notes.png" alt="screenshot: A Publish/unpublish note action is shown in the "Note properties" sidebar, just below a "Previous versions" button"></p>
|
||||
<p>Next, in the “publish note” dialog, click “Copy shareable link”. Notes can later be unpublished by clicking "Unpublish" in the publication dialog.</p>
|
||||
<h3>Viewing note history<a name="viewing-note-history" href="#viewing-note-history" class="heading-anchor">🔗</a></h3>
|
||||
<p>It is now possible to view and restore previous note versions from the mobile app. Like the "publish note" feature, previous note versions can be accessed from the note properties menu.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-note-history.png" alt="screenshot: The note history page"></p>
|
||||
<p>As on desktop, the note history feature can be configured from the “Note History” tab in settings.</p>
|
||||
<h3>Updated tag dialog<a name="updated-tag-dialog" href="#updated-tag-dialog" class="heading-anchor">🔗</a></h3>
|
||||
<p>The tag dialog has been redesigned, with a new UI for adding, removing, and creating new tags:<br>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-tag-editor.png" width="500" alt="screenshot: Tag dialog now consists of three sections: Added tags, Add new tags, Actions."/></p>
|
||||
<h3>Android: Improved voice typing<a name="android-improved-voice-typing" href="#android-improved-voice-typing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The voice typing feature on Android has been updated with <a href="https://github.com/laurent22/joplin/pull/12404">improved silence detection</a> and a new “<a href="https://github.com/laurent22/joplin/pull/12370">custom glossary</a>” setting. Voice typing also now <a href="https://github.com/laurent22/joplin/pull/12352">defaults to a more accurate (but somewhat slower) model</a>.</p>
|
||||
<h3>Quickly creating a note from multiple photos<a name="quickly-creating-a-note-from-multiple-photos" href="#quickly-creating-a-note-from-multiple-photos" class="heading-anchor">🔗</a></h3>
|
||||
<p>A “scan notebook” action has been added to the “New note” menu:</p>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-scan-notebook.png" width="500"/>
|
||||
<p>This action allows quickly creating a new note with multiple pictures taken from the camera.</p>
|
||||
<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
|
||||
<h3>More Markdown Editor settings<a name="more-markdown-editor-settings" href="#more-markdown-editor-settings" class="heading-anchor">🔗</a></h3>
|
||||
<p>The "Note" tab in settings now includes new settings for the Markdown editor, including:</p>
|
||||
<ul>
|
||||
<li>An option to render headers, lists, and certain other formatting within the editor.</li>
|
||||
<li>An option to render images in the editor.</li>
|
||||
</ul>
|
||||
<p>When enabled, these settings bring the Markdown editor closer to the Rich Text Editor, without <a href="https://joplinapp.org/help/apps/rich_text_editor">some of the Rich Text Editor's limitations</a>.</p>
|
||||
<p>These settings are also available on mobile.</p>
|
||||
<h3>Smaller application size and faster startup<a name="smaller-application-size-and-faster-startup" href="#smaller-application-size-and-faster-startup" class="heading-anchor">🔗</a></h3>
|
||||
<p>We've made the desktop application roughly 33% smaller! In addition to faster application startup, this means that the desktop app should be faster to download take up less space.</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Joplin version</th>
|
||||
<th>Previous size (v3.3.13)</th>
|
||||
<th>New size (v3.4.12)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Joplin for MacOS (ARM)</td>
|
||||
<td>211 MB</td>
|
||||
<td>141 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Windows (installer)</td>
|
||||
<td>321 MB</td>
|
||||
<td>219 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Windows (portable)</td>
|
||||
<td>320 MB</td>
|
||||
<td>219 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Linux (AppImage)</td>
|
||||
<td>219 MB</td>
|
||||
<td>147 MB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Terminal app<a name="terminal-app" href="#terminal-app" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Collapsible folders<a name="collapsible-folders" href="#collapsible-folders" class="heading-anchor">🔗</a></h3>
|
||||
<p>The <a href="https://joplinapp.org/help/apps/terminal/">terminal application</a> now supports expanding and collapsing folders by pressing <kbd>z</kbd>. For additional information, see <a href="https://github.com/laurent22/joplin/pull/12718">the original pull request</a>.</p>
|
||||
<h3>Managing shared notebooks and published notes<a name="managing-shared-notebooks-and-published-notes" href="#managing-shared-notebooks-and-published-notes" class="heading-anchor">🔗</a></h3>
|
||||
<p>New commands have been added to the terminal app, including <code>publish</code>, <code>unpublish</code>, and <code>share</code>. This allows the terminal app to manage shared folders and published notes.</p>
|
||||
<h2>Bug fixes<a name="bug-fixes" href="#bug-fixes" class="heading-anchor">🔗</a></h2>
|
||||
<p>For the full list of changes, see <a href="https://joplinapp.org/help/about/changelog/desktop/">the desktop changelog</a> and <a href="https://joplinapp.org/help/about/changelog/android/">the mobile changelog</a>.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20250922-release-3-4</link><guid isPermaLink="false">20250922-release-3-4</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Accessibility improvements<a name="accessibility-improvements" href="#accessibility-improvements" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Joplin 3.3 release introduces significant accessibility enhancements designed to make the application more inclusive and user-friendly. Users can now benefit from improved keyboard navigation, thanks to newly added shortcuts and clearer labels that streamline interaction across the interface. We've also added a "go to viewer" menu item that moves focus from the note editor to the note viewer. Focus is moved to the location in the viewer corresponding to the location of the cursor in the editor.</p>
|
||||
<p>Screen reader support has been bolstered, ensuring elements like the note list and sidebar are easier to toggle and interact with. These updates make the application more usable for individuals relying on assistive technologies.</p>
|
||||
@@ -446,10 +519,4 @@ sys 0m38.013s</p>
|
||||
<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><item><title><![CDATA[Joplin is hiring!]]></title><description><![CDATA[<p>Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.</p>
|
||||
<p>We are looking to hire two JavaScript software developers to work on the desktop, mobile, and server applications. All those are built using modern technologies, including React, React Native and Electron with a strong focus on test units.</p>
|
||||
<p>You need to demonstrate some experience with at least some of these technologies, and willing to learn more and touch various different projects.</p>
|
||||
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
|
||||
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
|
||||
<p>No agencies please.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item></channel></rss>
|
||||
]]></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>
|
||||
@@ -73,8 +73,10 @@ USER $user
|
||||
COPY --chown=$user:$user --from=builder /build/packages /home/$user/packages
|
||||
COPY --chown=$user:$user --from=builder /usr/bin/tini /usr/local/bin/tini
|
||||
|
||||
# Install pm2-logrotate and default settings as the runtime user
|
||||
RUN pm2 install pm2-logrotate \
|
||||
# We download a specific version of the plugin to prevent pm2 from fetching the latest, since it may
|
||||
# not have been properly audited (that fact was used to spread malware at some point). Ref:
|
||||
# https://github.com/laurent22/joplin/issues/12754
|
||||
RUN pm2 install https://registry.npmjs.org/pm2-logrotate/-/pm2-logrotate-3.0.0.tgz \
|
||||
&& pm2 set pm2-logrotate:max_size 100MB \
|
||||
&& pm2 set pm2-logrotate:retain 5 \
|
||||
&& pm2 set pm2-logrotate:compress true
|
||||
|
||||
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://damangameplay.in"><img title="Daman Game" width="256" src="https://joplinapp.org/images/sponsors/DamanGame.png" alt="Daman Game"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "23.10.0",
|
||||
"nodejs": "23.11.0",
|
||||
"pkg-config": "latest",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
# SLAVE_POSTGRES_PORT=5433
|
||||
# SLAVE_POSTGRES_HOST=localhost
|
||||
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
postgresql-master:
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
@@ -38,7 +36,7 @@ services:
|
||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||
|
||||
postgresql-slave:
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
||||
ports:
|
||||
- '5433:5432'
|
||||
depends_on:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# This compose file can be used in development to run both the database and app
|
||||
# within Docker.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
|
||||
# - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
|
||||
|
||||
version: '3'
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
transcribe-network:
|
||||
|
||||
15
package.json
@@ -79,17 +79,17 @@
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.5.2",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.4",
|
||||
@@ -101,7 +101,6 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
@@ -118,6 +117,12 @@
|
||||
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/",
|
||||
"node-gyp@npm:^9.0.0": "11.2.0"
|
||||
"node-gyp@npm:^9.0.0": "11.2.0",
|
||||
"depd@npm:^2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:~2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:~1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:^1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:^1.1.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,44 @@
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
import * as fs from 'fs-extra';
|
||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
import { dirname } from '@joplin/lib/path-utils';
|
||||
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
|
||||
const JoplinDatabase = require('@joplin/lib/JoplinDatabase').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const Note = require('@joplin/lib/models/Note').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const exec = require('child_process').exec;
|
||||
const nodeSqlite = require('sqlite3');
|
||||
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
||||
const { default: shimInitCli } = require('./utils/shimInitCli');
|
||||
|
||||
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
|
||||
const joplinAppPath = `${__dirname}/main.js`;
|
||||
|
||||
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null });
|
||||
require('@joplin/lib/testing/test-utils');
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_ERROR);
|
||||
|
||||
const dbLogger = new Logger();
|
||||
dbLogger.addTarget('console');
|
||||
dbLogger.addTarget(TargetType.Console);
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO);
|
||||
|
||||
const db = new JoplinDatabase(new DatabaseDriverNode());
|
||||
db.setLogger(dbLogger);
|
||||
|
||||
function createClient(id) {
|
||||
interface Client {
|
||||
id: number;
|
||||
profileDir: string;
|
||||
}
|
||||
|
||||
function createClient(id: number): Client {
|
||||
return {
|
||||
id: id,
|
||||
profileDir: `${baseDir}/client${id}`,
|
||||
@@ -37,13 +48,13 @@ function createClient(id) {
|
||||
|
||||
const client = createClient(1);
|
||||
|
||||
function execCommand(client, command) {
|
||||
function execCommand(client: Client, command: string) {
|
||||
const exePath = `node ${joplinAppPath}`;
|
||||
const cmd = `${exePath} --update-geolocation-disabled --env dev --profile ${client.profileDir} ${command}`;
|
||||
logger.info(`${client.id}: ${command}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
exec(cmd, (error: string, stdout: string, stderr: string) => {
|
||||
if (error) {
|
||||
logger.error(stderr);
|
||||
reject(error);
|
||||
@@ -54,17 +65,17 @@ function execCommand(client, command) {
|
||||
});
|
||||
}
|
||||
|
||||
function assertTrue(v) {
|
||||
function assertTrue(v: unknown) {
|
||||
if (!v) throw new Error(sprintf('Expected "true", got "%s"."', v));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
|
||||
function assertFalse(v) {
|
||||
function assertFalse(v: unknown) {
|
||||
if (v) throw new Error(sprintf('Expected "false", got "%s"."', v));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
|
||||
function assertEquals(expected, real) {
|
||||
function assertEquals(expected: unknown, real: unknown) {
|
||||
if (expected !== real) throw new Error(sprintf('Expecting "%s", got "%s"', expected, real));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
@@ -73,7 +84,7 @@ async function clearDatabase() {
|
||||
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
|
||||
}
|
||||
|
||||
const testUnits = {};
|
||||
const testUnits: Record<string, ()=> Promise<void>> = {};
|
||||
|
||||
testUnits.testFolders = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
@@ -85,10 +96,16 @@ testUnits.testFolders = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(1, folders.length);
|
||||
assertEquals(2, folders.length);
|
||||
assertEquals('nb1', folders[0].title);
|
||||
assertEquals('nb1', folders[1].title);
|
||||
|
||||
await execCommand(client, 'rm -r -f nb1');
|
||||
await execCommand(client, 'rmbook -p -f nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(1, folders.length);
|
||||
|
||||
await execCommand(client, 'rmbook -p -f nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(0, folders.length);
|
||||
@@ -102,7 +119,7 @@ testUnits.testNotes = async () => {
|
||||
assertEquals(1, notes.length);
|
||||
assertEquals('n1', notes[0].title);
|
||||
|
||||
await execCommand(client, 'rm -f n1');
|
||||
await execCommand(client, 'rmnote -p -f n1');
|
||||
notes = await Note.all();
|
||||
assertEquals(0, notes.length);
|
||||
|
||||
@@ -112,12 +129,19 @@ testUnits.testNotes = async () => {
|
||||
notes = await Note.all();
|
||||
assertEquals(2, notes.length);
|
||||
|
||||
await execCommand(client, 'rm -f \'blabla*\'');
|
||||
// Should fail to delete a non-existent note
|
||||
let failed = false;
|
||||
try {
|
||||
await execCommand(client, 'rmnote -f \'blabla*\'');
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
}
|
||||
assertEquals(failed, true);
|
||||
|
||||
notes = await Note.all();
|
||||
assertEquals(2, notes.length);
|
||||
|
||||
await execCommand(client, 'rm -f \'n*\'');
|
||||
await execCommand(client, 'rmnote -f -p \'n*\'');
|
||||
|
||||
notes = await Note.all();
|
||||
assertEquals(0, notes.length);
|
||||
@@ -140,10 +164,12 @@ testUnits.testCat = async () => {
|
||||
|
||||
testUnits.testConfig = async () => {
|
||||
await execCommand(client, 'config editor vim');
|
||||
await Setting.reset();
|
||||
await Setting.load();
|
||||
assertEquals('vim', Setting.value('editor'));
|
||||
|
||||
await execCommand(client, 'config editor subl');
|
||||
await Setting.reset();
|
||||
await Setting.load();
|
||||
assertEquals('subl', Setting.value('editor'));
|
||||
|
||||
@@ -201,15 +227,47 @@ testUnits.testMv = async () => {
|
||||
await execCommand(client, 'mknote note2');
|
||||
await execCommand(client, 'mknote note3');
|
||||
await execCommand(client, 'mknote blabla');
|
||||
await execCommand(client, 'mv \'note*\' nb2');
|
||||
|
||||
notes1 = await Note.previews(f1.id);
|
||||
notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(4, notes1.length);
|
||||
assertEquals(1, notes2.length);
|
||||
|
||||
await execCommand(client, 'mv \'note*\' nb2');
|
||||
|
||||
notes2 = await Note.previews(f2.id);
|
||||
notes1 = await Note.previews(f1.id);
|
||||
|
||||
assertEquals(1, notes1.length);
|
||||
assertEquals(4, notes2.length);
|
||||
};
|
||||
|
||||
testUnits.testUse = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
await execCommand(client, 'mkbook nb2');
|
||||
await execCommand(client, 'mknote n1');
|
||||
await execCommand(client, 'mknote n2');
|
||||
|
||||
const f1 = await Folder.loadByTitle('nb1');
|
||||
const f2 = await Folder.loadByTitle('nb2');
|
||||
let notes1 = await Note.previews(f1.id);
|
||||
let notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(0, notes1.length);
|
||||
assertEquals(2, notes2.length);
|
||||
|
||||
await execCommand(client, 'use nb1');
|
||||
await execCommand(client, 'mknote note2');
|
||||
await execCommand(client, 'mknote note3');
|
||||
|
||||
notes1 = await Note.previews(f1.id);
|
||||
notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(2, notes1.length);
|
||||
assertEquals(2, notes2.length);
|
||||
};
|
||||
|
||||
async function main() {
|
||||
await fs.remove(baseDir);
|
||||
|
||||
@@ -217,7 +275,9 @@ async function main() {
|
||||
|
||||
await db.open({ name: `${client.profileDir}/database.sqlite` });
|
||||
BaseModel.setDb(db);
|
||||
await Setting.load();
|
||||
Setting.setConstant('rootProfileDir', client.profileDir);
|
||||
Setting.setConstant('profileDir', client.profileDir);
|
||||
await loadKeychainServiceAndSettings([]);
|
||||
|
||||
let onlyThisTest = 'testMv';
|
||||
onlyThisTest = '';
|
||||
@@ -234,7 +294,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main(process.argv).catch(error => {
|
||||
main().catch(error => {
|
||||
console.info('');
|
||||
logger.error(error);
|
||||
});
|
||||
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.4.1",
|
||||
"version": "3.5.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"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.2",
|
||||
"sharp": "0.34.3",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -70,14 +70,14 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/node": "18.19.119",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import * as fs from 'fs-extra';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -310,7 +310,7 @@ describe('services_PluginService', () => {
|
||||
|
||||
let resetPlatformMock = () => {};
|
||||
if (!isDesktop) {
|
||||
resetPlatformMock = mockMobilePlatform('android').reset;
|
||||
resetPlatformMock = mockMobilePlatform(MobilePlatform.Android).reset;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
|
||||
@@ -407,7 +407,17 @@ export default class ElectronAppWrapper {
|
||||
isGoingToExit = true;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
|
||||
const w = this.win_;
|
||||
if (!w) return;
|
||||
|
||||
if (w.isFullScreen()) {
|
||||
// leave fullscreen, then hide
|
||||
w.once('leave-full-screen', () => w.hide());
|
||||
w.setFullScreen(false);
|
||||
} else {
|
||||
w.hide();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const hasBackgroundWindows = this.secondaryWindows_.size > 0;
|
||||
@@ -612,7 +622,11 @@ export default class ElectronAppWrapper {
|
||||
console.warn('The window object was not available during the click event from tray icon');
|
||||
return;
|
||||
}
|
||||
this.mainWindow().show();
|
||||
if (!this.mainWindow().isVisible()) {
|
||||
this.mainWindow().show();
|
||||
} else {
|
||||
this.mainWindow().hide();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot create tray', error);
|
||||
|
||||
@@ -225,7 +225,7 @@ const Button = React.forwardRef(({
|
||||
animation={iconAnimation}
|
||||
mr={iconOnly ? '0' : '6px'}
|
||||
color={color}
|
||||
className={iconName}
|
||||
className={`${iconName} icon`}
|
||||
role='img'
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled = 1,
|
||||
@@ -150,8 +151,7 @@ export default function(props: Props) {
|
||||
|
||||
const onNameClick = useCallback(() => {
|
||||
const manifest = item.manifest;
|
||||
if (!manifest.homepage_url) return;
|
||||
void bridge().openExternal(manifest.homepage_url);
|
||||
void bridge().openExternal(getPluginHelpUrl(manifest.id));
|
||||
}, [item]);
|
||||
|
||||
const onRecommendedClick = useCallback(() => {
|
||||
|
||||
@@ -1387,16 +1387,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function onCopy(event: any) {
|
||||
const copiedContent = editor.selection.getContent();
|
||||
if (!copiedContent) return;
|
||||
copyHtmlToClipboard(copiedContent);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function onCut(event: any) {
|
||||
event.preventDefault();
|
||||
const selectedContent = editor.selection.getContent();
|
||||
if (!selectedContent) return;
|
||||
copyHtmlToClipboard(selectedContent);
|
||||
editor.insertContent('');
|
||||
event.preventDefault();
|
||||
onChangeHandler();
|
||||
}
|
||||
|
||||
@@ -1444,7 +1446,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// `compositionend` means that a user has finished entering a Chinese
|
||||
// (or other languages that require IME) character.
|
||||
editor.on(TinyMceEditorEvents.CompositionEnd, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Cut, onCut);
|
||||
editor.on(TinyMceEditorEvents.Cut, onCut, true);
|
||||
editor.on(TinyMceEditorEvents.JoplinChange, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Undo, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
|
||||
@@ -15,6 +15,7 @@ const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
const { clipboard } = require('electron');
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
const md5 = require('md5');
|
||||
const path = require('path');
|
||||
|
||||
@@ -43,22 +44,30 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
|
||||
if (!filePaths || !filePaths.length) return null;
|
||||
}
|
||||
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = getCollator(collatorLocale);
|
||||
filePaths = filePaths.sort((a, b) => {
|
||||
return collator.compare(a, b);
|
||||
});
|
||||
|
||||
let pos = options.position ?? 0;
|
||||
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
const beforeLen = body.length;
|
||||
try {
|
||||
logger.info(`Attaching ${filePath}`);
|
||||
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
|
||||
const newBody = await shim.attachFileToNoteBody(body, filePath, pos, {
|
||||
createFileURL: options.createFileURL,
|
||||
resizeLargeImages: Setting.value('imageResizing'),
|
||||
markupLanguage: options.markupLanguage,
|
||||
resourceSuffix: i > 0 ? ' ' : '',
|
||||
resourcePrefix: i > 0 ? ' ' : '',
|
||||
});
|
||||
|
||||
if (!newBody) {
|
||||
logger.info('File attachment was cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
pos += newBody.length - beforeLen;
|
||||
body = newBody;
|
||||
logger.info('File was attached.');
|
||||
} catch (error) {
|
||||
@@ -66,7 +75,6 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,13 @@ interface Props {
|
||||
const SidebarComponent = (props: Props) => {
|
||||
const renderSynchronizeButton = (type: string) => {
|
||||
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
||||
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
|
||||
|
||||
return (
|
||||
<StyledSynchronizeButton
|
||||
level={ButtonLevel.SidebarSecondary}
|
||||
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'}`}
|
||||
iconName="icon-sync"
|
||||
key="sync_button"
|
||||
iconAnimation={iconAnimation}
|
||||
title={label}
|
||||
onClick={() => {
|
||||
void CommandService.instance().execute('synchronize', type !== 'sync');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
|
||||
import { HeaderId, HeaderListItem } from '../types';
|
||||
import bridge from '../../../services/bridge';
|
||||
@@ -25,6 +25,8 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
const item = props.item;
|
||||
const onItemClick = item.onClick;
|
||||
const itemId = item.id;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const expanded = item.expanded;
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
|
||||
if (onItemClick) {
|
||||
@@ -44,6 +46,14 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
}
|
||||
}, [itemId]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
containerRef={props.anchorRef}
|
||||
@@ -58,8 +68,12 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
{...item.extraProps}
|
||||
onDrop={props.onDrop}
|
||||
>
|
||||
<StyledHeader onClick={onClick}>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
|
||||
<StyledHeader
|
||||
onClick={onClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={isHovered ? `fas ${expanded ? 'fa-caret-down' : 'fa-caret-right'}` : item.iconName}/>
|
||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||
</StyledHeader>
|
||||
</ListItemWrapper>
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
@use 'styles/sidebar-expand-link.scss';
|
||||
@use 'styles/sidebar-header-container.scss';
|
||||
@use 'styles/sidebar-spacer-item.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
||||
@use 'styles/sidebar-sync-button.scss';
|
||||
@@ -28,6 +28,11 @@ export const StyledHeader = styled.div`
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
//cursor: pointer;
|
||||
cursor: default;
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: ${(props: StyleProps) => props.theme.backgroundColorHover2};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledHeaderIcon = styled.i`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
@keyframes icon-infinite-rotation {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sync-button {
|
||||
&.-syncing > .icon {
|
||||
animation: icon-infinite-rotation 1s linear infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export default class NoteListUtils {
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
|
||||
if ([9, 10].includes(Setting.value('sync.target'))) {
|
||||
if ([9, 10, 11].includes(Setting.value('sync.target'))) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!--
|
||||
No CPS because we need to allow everything due to some dependencies (eg. depd, which comes from maybe Node or Electron
|
||||
uses 'eval'.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
|
||||
-->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
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://* ;
|
||||
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://* ;
|
||||
font-src 'self' http://* https://* blob: data: joplin-content://* ;
|
||||
"
|
||||
/>
|
||||
<title>Joplin</title>
|
||||
<!-- Note: Add new dynamic CSS imports to style.scss to allow them to be included in secondary windows. -->
|
||||
<link rel="stylesheet" href="style.min.css">
|
||||
|
||||
@@ -8,6 +8,7 @@ import getMainWindow from './util/getMainWindow';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
import getImageSourceSize from './util/getImageSourceSize';
|
||||
import setSettingValue from './util/setSettingValue';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
@@ -19,6 +20,13 @@ test.describe('main', () => {
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('app should support French localization', async ({ mainWindow, electronApp }) => {
|
||||
await setSettingValue(electronApp, mainWindow, 'locale', 'fr_FR');
|
||||
// The "Notebooks" header should be localized
|
||||
const localizedText = mainWindow.getByText('Carnets').first();
|
||||
await expect(localizedText).toBeAttached();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const editor = await mainScreen.createNewNote('Test note');
|
||||
|
||||
@@ -97,12 +97,6 @@ a {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@keyframes icon-infinite-rotation{
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rdtPicker {
|
||||
min-width: 250px;
|
||||
width: auto !important;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.4.11",
|
||||
"version": "3.5.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -46,6 +46,7 @@
|
||||
"asar": true,
|
||||
"asarUnpack": "./node_modules/node-notifier/vendor/**",
|
||||
"win": {
|
||||
"sign": "./sign.js",
|
||||
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
|
||||
"icon": "../../Assets/ImageSources/Joplin.ico",
|
||||
"target": [
|
||||
@@ -131,28 +132,27 @@
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"7zip-bin": "5.2.0",
|
||||
"@axe-core/playwright": "4.10.1",
|
||||
"@axe-core/playwright": "4.10.2",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.4",
|
||||
"@joplin/editor": "~3.4",
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@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",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@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.103",
|
||||
"@types/node": "18.19.119",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"codemirror": "5.65.9",
|
||||
@@ -166,7 +166,7 @@
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
"highlight.js": "11.11.1",
|
||||
"immer": "9.0.21",
|
||||
@@ -187,7 +187,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-redux": "8.1.3",
|
||||
"react-select": "5.10.1",
|
||||
"react-select": "5.10.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
@@ -199,15 +199,15 @@
|
||||
"styled-components": "5.3.11",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.1.1",
|
||||
"tesseract.js": "6.0.1",
|
||||
"tinymce": "6.8.5",
|
||||
"ts-jest": "29.3.1",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
"@joplin/onenote-converter": "~3.4",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
|
||||
109
packages/app-desktop/sign.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { chdir, cwd } = require('process');
|
||||
const { mkdirpSync, moveSync, pathExists } = require('fs-extra');
|
||||
const { readdirSync, writeFileSync } = require('fs');
|
||||
const { dirname } = require('path');
|
||||
|
||||
const signToolName = 'CodeSignTool.bat';
|
||||
|
||||
const getTempDir = () => {
|
||||
if (process.env.RUNNER_TEMP) return process.env.RUNNER_TEMP;
|
||||
if (process.env.GITHUB_WORKSPACE) return process.env.GITHUB_WORKSPACE;
|
||||
|
||||
const output = `${dirname(dirname(__dirname))}/temp`;
|
||||
mkdirpSync(output);
|
||||
return output;
|
||||
};
|
||||
|
||||
const tempDir = getTempDir();
|
||||
|
||||
const downloadSignTool = async () => {
|
||||
const signToolUrl = 'https://www.ssl.com/download/codesigntool-for-windows/';
|
||||
const downloadDir = `${tempDir}/signToolDownloadTemp`;
|
||||
const extractDir = `${tempDir}/signToolExtractTemp`;
|
||||
|
||||
if (await pathExists(`${extractDir}/${signToolName}`)) {
|
||||
console.info('sign.js: Sign tool has already been downloaded - skipping');
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
mkdirpSync(downloadDir);
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
const response = await fetch(signToolUrl);
|
||||
if (!response.ok) throw new Error(`sign.js: HTTP error ${response.status}: ${response.statusText}`);
|
||||
|
||||
const zipPath = `${downloadDir}/codeSignTool.zip`;
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
writeFileSync(zipPath, buffer);
|
||||
|
||||
console.info('sign.js: Downloaded sign tool zip:', readdirSync(downloadDir));
|
||||
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
execSync(
|
||||
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
console.info('sign.js: Extracted sign tool zip:', readdirSync(extractDir));
|
||||
|
||||
return extractDir;
|
||||
};
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
const inputFilePath = configuration.path;
|
||||
|
||||
const {
|
||||
SSL_ESIGNER_USER_NAME,
|
||||
SSL_ESIGNER_USER_PASSWORD,
|
||||
SSL_ESIGNER_CREDENTIAL_ID,
|
||||
SSL_ESIGNER_USER_TOTP,
|
||||
SIGN_APPLICATION,
|
||||
} = process.env;
|
||||
|
||||
console.info('sign.js: File to sign:', inputFilePath);
|
||||
|
||||
console.info('sign.js: Using temp dir:', tempDir);
|
||||
|
||||
if (SIGN_APPLICATION !== '1') {
|
||||
console.info('sign.js: SIGN_APPLICATION != 1 - not signing application');
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('sign.js: SIGN_APPLICATION = 1 - signing application');
|
||||
|
||||
const signToolDir = await downloadSignTool();
|
||||
const signToolOutDir = `${tempDir}/signedToolOutDir`;
|
||||
mkdirpSync(signToolOutDir);
|
||||
|
||||
const previousDir = cwd();
|
||||
chdir(signToolDir);
|
||||
|
||||
try {
|
||||
const cmd = [
|
||||
`${signToolName} sign`,
|
||||
`-input_file_path="${inputFilePath}"`,
|
||||
`-output_dir_path="${signToolOutDir}"`,
|
||||
`-credential_id="${SSL_ESIGNER_CREDENTIAL_ID}"`,
|
||||
`-username="${SSL_ESIGNER_USER_NAME}"`,
|
||||
`-password="${SSL_ESIGNER_USER_PASSWORD}"`,
|
||||
`-totp_secret="${SSL_ESIGNER_USER_TOTP}"`,
|
||||
];
|
||||
|
||||
execSync(cmd.join(' '));
|
||||
|
||||
const createdFiles = readdirSync(signToolOutDir);
|
||||
console.info('sign.js: Created files:', createdFiles);
|
||||
|
||||
moveSync(`${signToolOutDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('sign.js: Could not sign file:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
chdir(previousDir);
|
||||
}
|
||||
};
|
||||
@@ -90,7 +90,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097780
|
||||
versionName "3.4.7"
|
||||
versionName "3.5.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -24,29 +24,8 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
// Seems to be required for react-native-vosk, otherwise the lib looks for it at "https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar" but it's not there. And we get this error:
|
||||
//
|
||||
// Execution failed for task ':app:checkDebugAarMetadata'.
|
||||
// > Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
|
||||
// > Failed to transform vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46) to match attributes {artifactType=android-aar-metadata, org.gradle.status=release}.
|
||||
// > Could not find vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46).
|
||||
// Searched in the following locations:
|
||||
// https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar
|
||||
//
|
||||
// But according to this page, the lib is on the Apache repository:
|
||||
//
|
||||
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
|
||||
maven { url "https://maven.apache.org" }
|
||||
|
||||
// Also required for react-native-vosk?
|
||||
maven { url "https://maven.google.com" }
|
||||
|
||||
// Maybe still needed to fetch above package?
|
||||
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
// expo-camera bundles a custom com.google.android:cameraview
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Platform, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { BarcodeScanner } from './utils/useBarcodeScanner';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { Chip, Text } from 'react-native-paper';
|
||||
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -84,7 +84,7 @@ const ScannedBarcodes: React.FC<Props> = props => {
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
themeId={props.themeId}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
>
|
||||
<ScrollView>
|
||||
<Text variant='titleMedium' role='heading'>{_('Scanned code')}</Text>
|
||||
|
||||
@@ -66,12 +66,12 @@ describe('ComboBox', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('changing the search query should limit which items are visible', () => {
|
||||
test('changing the search query should limit which items are visible and be case insensitive', () => {
|
||||
const testItems = [
|
||||
{ title: 'a' },
|
||||
{ title: 'b' },
|
||||
{ title: 'c' },
|
||||
{ title: 'aa' },
|
||||
{ title: 'Aa' },
|
||||
];
|
||||
const { unmount } = render(
|
||||
<WrappedComboBox items={testItems}/>,
|
||||
@@ -82,7 +82,7 @@ describe('ComboBox', () => {
|
||||
|
||||
const updatedResults = getSearchResults();
|
||||
expect(updatedResults[0]).toHaveTextContent('a');
|
||||
expect(updatedResults[1]).toHaveTextContent('aa');
|
||||
expect(updatedResults[1]).toHaveTextContent('Aa');
|
||||
expect(updatedResults).toHaveLength(2);
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -12,7 +12,7 @@ import focusView from '../utils/focusView';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import NestableFlatList, { NestableFlatListControl } from './NestableFlatList';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
const naturalCompare = require('string-natural-compare');
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
|
||||
|
||||
export interface Option {
|
||||
@@ -64,17 +64,20 @@ interface UseSearchResultsOptions {
|
||||
const useSearchResults = ({
|
||||
search, setSearch, options, onAddItem, canAddItem,
|
||||
}: UseSearchResultsOptions) => {
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const results = useMemo(() => {
|
||||
const collator = getCollator(collatorLocale);
|
||||
const lowerSearch = search?.toLowerCase();
|
||||
return options
|
||||
.filter(option => option.title.toLowerCase().includes(search))
|
||||
.filter(option => option.title.toLowerCase().includes(lowerSearch))
|
||||
.sort((a, b) => {
|
||||
if (a.title === b.title) return 0;
|
||||
// Full matches should go first
|
||||
if (a.title === search) return -1;
|
||||
if (b.title === search) return 1;
|
||||
return naturalCompare(a.title, b.title);
|
||||
if (a.title.toLowerCase() === lowerSearch) return -1;
|
||||
if (b.title.toLowerCase() === lowerSearch) return 1;
|
||||
return collator.compare(a.title, b.title);
|
||||
});
|
||||
}, [search, options]);
|
||||
}, [search, options, collatorLocale]);
|
||||
|
||||
const canAdd = (
|
||||
!!onAddItem
|
||||
|
||||
@@ -6,7 +6,10 @@ import { themeStyle } from './global-style';
|
||||
import Modal from './Modal';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export enum DialogSize {
|
||||
export enum DialogVariant {
|
||||
// Small width, auto-determined height
|
||||
SmallResize = 'small-resize',
|
||||
|
||||
Small = 'small',
|
||||
|
||||
// Ideal for panels and dialogs that should be fullscreen even on large devices
|
||||
@@ -20,34 +23,58 @@ interface Props {
|
||||
containerStyle?: ViewStyle;
|
||||
children: React.ReactNode;
|
||||
heading?: string;
|
||||
scrollOverflow?: boolean;
|
||||
|
||||
size: DialogSize;
|
||||
size: DialogVariant;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize) => {
|
||||
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogVariant) => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const maxWidth = size === DialogSize.Large ? windowSize.width : 500;
|
||||
const maxHeight = size === DialogSize.Large ? windowSize.height : 700;
|
||||
const maxWidth = size === DialogVariant.Large ? windowSize.width : 500;
|
||||
const maxHeight = size === DialogVariant.Large ? windowSize.height : 700;
|
||||
|
||||
const dialogSizing: ViewStyle = {
|
||||
width: '100%',
|
||||
|
||||
...(size !== DialogVariant.SmallResize ? {
|
||||
height: '100%',
|
||||
} : { }),
|
||||
};
|
||||
|
||||
return StyleSheet.create({
|
||||
closeButtonContainer: {
|
||||
closeButtonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
closeButtonRowWithHeading: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
closeButton: {
|
||||
margin: 0,
|
||||
},
|
||||
// Ensure that the close button is aligned with the center of the header:
|
||||
// Make its container smaller and center it.
|
||||
closeButtonWrapper: {
|
||||
height: 18,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
heading: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
modalBackground: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dialogContainer: {
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexShrink: 1,
|
||||
...dialogSizing,
|
||||
|
||||
// Center
|
||||
marginLeft: 'auto',
|
||||
@@ -58,11 +85,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
|
||||
...containerStyle,
|
||||
},
|
||||
dialogSurface: {
|
||||
borderRadius: 12,
|
||||
borderRadius: 24,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 24,
|
||||
...dialogSizing,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowSize.width, windowSize.height, containerStyle, size]);
|
||||
@@ -76,13 +103,16 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
<Text variant='headlineSmall' role='heading' style={styles.heading}>{props.heading}</Text>
|
||||
) : null;
|
||||
const closeButtonRow = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
<View style={[styles.closeButtonRow, !!props.heading && styles.closeButtonRowWithHeading]}>
|
||||
{heading ?? <View/>}
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onDismiss}
|
||||
/>
|
||||
<View style={styles.closeButtonWrapper}>
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onDismiss}
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -92,9 +122,13 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
onDismiss={props.onDismiss}
|
||||
onRequestClose={props.onDismiss}
|
||||
containerStyle={styles.dialogContainer}
|
||||
modalBackgroundStyle={styles.modalBackground}
|
||||
animationType='fade'
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
transparent={true}
|
||||
scrollOverflow={props.scrollOverflow}
|
||||
// Allows the modal background to extend under the statusbar
|
||||
statusBarTranslucent
|
||||
>
|
||||
<Surface style={styles.dialogSurface} elevation={1}>
|
||||
{closeButtonRow}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AppState } from '../../utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import allToolbarCommandNamesFromState from './utils/allToolbarCommandNamesFromState';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import selectedCommandNamesFromState from './utils/selectedCommandNamesFromState';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { DeleteButton } from '../buttons';
|
||||
@@ -158,7 +158,7 @@ const ToolbarEditorScreen: React.FC<EditorDialogProps> = props => {
|
||||
|
||||
return (
|
||||
<DismissibleDialog
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
|
||||
@@ -7,6 +7,7 @@ import createMockReduxStore from '../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../utils/testing/setupGlobalStore';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import FeedbackBanner from './FeedbackBanner';
|
||||
import { MobilePlatform } from '@joplin/lib/shim';
|
||||
|
||||
interface WrapperProps { }
|
||||
|
||||
@@ -84,7 +85,7 @@ describe('FeedbackBanner', () => {
|
||||
setupGlobalStore(store);
|
||||
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
mockMobilePlatform('web');
|
||||
mockMobilePlatform(MobilePlatform.Web);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -93,9 +94,9 @@ describe('FeedbackBanner', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ platform: 'android', shouldShow: false },
|
||||
{ platform: 'web', shouldShow: true },
|
||||
{ platform: 'ios', shouldShow: false },
|
||||
{ platform: MobilePlatform.Android, shouldShow: false },
|
||||
{ platform: MobilePlatform.Web, shouldShow: true },
|
||||
{ platform: MobilePlatform.Ios, shouldShow: false },
|
||||
])('should correctly show/hide the feedback banner on %s', ({ platform, shouldShow }) => {
|
||||
mockMobilePlatform(platform);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ function NoteBodyViewer(props: Props) {
|
||||
onResourceLongPress,
|
||||
});
|
||||
|
||||
const { api: renderer, pageSetup, webViewEventHandlers } = useWebViewSetup({
|
||||
const { api: renderer, pageSetup, webViewEventHandlers, hasPluginScripts } = useWebViewSetup({
|
||||
webviewRef,
|
||||
onBodyScroll: onScroll,
|
||||
onPostMessage,
|
||||
@@ -106,6 +106,7 @@ function NoteBodyViewer(props: Props) {
|
||||
mixedContentMode="always"
|
||||
onLoadEnd={onLoadEnd}
|
||||
onMessage={webViewEventHandlers.onMessage}
|
||||
hasPluginScripts={hasPluginScripts}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,9 @@ import { toFileExtension } from '@joplin/lib/mime-utils';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import WarningBanner from './WarningBanner';
|
||||
import useIsScreenReaderEnabled from '../../utils/hooks/useIsScreenReaderEnabled';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
@@ -87,6 +90,8 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
const noteEditorSearchChangeSource = 'joplin.noteEditor.setSearchState';
|
||||
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
@@ -101,7 +106,7 @@ const useEditorControl = (
|
||||
};
|
||||
|
||||
const setSearchStateCallback = (state: SearchState) => {
|
||||
editorRef.current.setSearchState(state);
|
||||
editorRef.current.setSearchState(state, noteEditorSearchChangeSource);
|
||||
setSearchState(state);
|
||||
};
|
||||
|
||||
@@ -111,6 +116,7 @@ const useEditorControl = (
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
execCommand(command, ...args: any[]) {
|
||||
logger.debug('execCommand', command);
|
||||
return editorRef.current.execCommand(command, ...args);
|
||||
},
|
||||
|
||||
@@ -306,15 +312,26 @@ function NoteEditor(props: Props) {
|
||||
case EditorEventType.FollowLink:
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
break;
|
||||
case EditorEventType.UpdateSearchDialog:
|
||||
setSearchState(event.searchState);
|
||||
case EditorEventType.UpdateSearchDialog: {
|
||||
const hasExternalChange = (
|
||||
event.changeSources.length !== 1
|
||||
|| event.changeSources[0] !== noteEditorSearchChangeSource
|
||||
);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
// If the change to the search was done by this editor, it was already applied to the
|
||||
// search state. Skipping the update in this case also helps avoid overwriting the
|
||||
// search state with an older value.
|
||||
if (hasExternalChange) {
|
||||
setSearchState(event.searchState);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditorEventType.Remove:
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorSettings } from './types';
|
||||
import { pregQuote } from '@joplin/lib/string-utils';
|
||||
import { join } from 'path';
|
||||
|
||||
|
||||
interface WrapperProps {
|
||||
@@ -103,8 +104,8 @@ const mockTyping = (window: EditorWindow, text: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const mockSelectionMovement = (window: EditorWindow, position: number) => {
|
||||
getEditorControl(window).select(position, position);
|
||||
const mockSelectionMovement = (window: EditorWindow, from: number, to?: number) => {
|
||||
getEditorControl(window).select(from, to ?? from);
|
||||
};
|
||||
|
||||
const findElement = async function<ElementType extends Element = Element>(selector: string) {
|
||||
@@ -333,7 +334,7 @@ describe('RichTextEditor', () => {
|
||||
const editorContent = body.trim();
|
||||
if (markupLanguage === MarkupLanguage.Html) {
|
||||
expect(editorContent).toMatch(
|
||||
new RegExp(`^<p><img src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
|
||||
new RegExp(`^<p><img[^>]* src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
|
||||
);
|
||||
} else {
|
||||
expect(editorContent).toBe(` test`);
|
||||
@@ -341,6 +342,29 @@ describe('RichTextEditor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve non-image attachments on edit', async () => {
|
||||
const { note, resource } = await createNoteAndResource({ path: join(supportDir, 'sample.txt') });
|
||||
let body = note.body;
|
||||
|
||||
const resources = await attachedResources(body);
|
||||
render(<WrappedEditor
|
||||
noteBody={note.body}
|
||||
note={note}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
noteResources={resources}
|
||||
/>);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' test');
|
||||
|
||||
await waitFor(async () => {
|
||||
const editorContent = body.trim();
|
||||
// TODO: At present, the resource title may be included in the final Markdown
|
||||
// (e.g. as [sample.txt](:/id-here "sample.txt")).
|
||||
expect(editorContent).toMatch(new RegExp(`^\\[sample\\.txt\\]\\(:/${pregQuote(resource.id)}.*\\) test$`));
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ useValidSyntax: false },
|
||||
{ useValidSyntax: true },
|
||||
@@ -390,14 +414,18 @@ describe('RichTextEditor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible show an editor for math blocks', async () => {
|
||||
it('should be possible to show an editor for math blocks', async () => {
|
||||
let body = 'Test:\n\n$$3^2 + 4^2 = 5^2$$';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const editButton = await findElement<HTMLButtonElement>('button.edit');
|
||||
const window = await getEditorWindow();
|
||||
// Select the math block to show the "edit" button.
|
||||
mockSelectionMovement(window, '<Test:>'.length, '<Test:>$'.length);
|
||||
|
||||
const editButton = await findElement<HTMLButtonElement>('button.edit-button');
|
||||
editButton.click();
|
||||
|
||||
const editor = await findElement('dialog .cm-editor');
|
||||
@@ -452,6 +480,8 @@ describe('RichTextEditor', () => {
|
||||
'==highlight==ed',
|
||||
'<sup>Super</sup>script',
|
||||
'<sub>Sub</sub>script',
|
||||
'',
|
||||
'<img width="120" src="data:image/svg+xml;utf8,test">',
|
||||
])('should preserve inline markup on edit (case %#)', async (initialBody) => {
|
||||
initialBody += 'test'; // Ensure that typing will add new content outside the formatting
|
||||
let body = initialBody;
|
||||
|
||||
@@ -44,6 +44,13 @@ function useCss(themeId: number, editorCss: string): string {
|
||||
font-size: 13pt;
|
||||
font-family: ${JSON.stringify(theme.fontFamily)}, sans-serif;
|
||||
}
|
||||
|
||||
.RichTextEditor {
|
||||
/* Relatively positioning the editor container causes absolutely-positioned
|
||||
elements to be positioned relative to Rich Text Editor's container,
|
||||
rather than the body. This fixes an alignment issue involving button overlays. */
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
}, [themeId, editorCss]);
|
||||
}
|
||||
|
||||
@@ -158,8 +158,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
const state = props.searchState;
|
||||
const control = props.searchControl;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const updateSearchState = (changedData: any) => {
|
||||
const updateSearchState = (changedData: Partial<SearchState>) => {
|
||||
const newState = { ...state, ...changedData };
|
||||
control.setSearchState(newState);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Text } from 'react-native-paper';
|
||||
import IconButton from '../IconButton';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useState } from 'react';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import { LinkButton } from '../buttons';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
||||
import getPackageInfo from '../../utils/getPackageInfo';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
@@ -30,7 +30,10 @@ const onReportBug = () => {
|
||||
const styles = StyleSheet.create({
|
||||
feedbackContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
justifyContent: 'flex-end',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
paragraph: {
|
||||
paddingBottom: 7,
|
||||
@@ -65,7 +68,7 @@ const WebBetaButton: React.FC<Props> = props => {
|
||||
/>
|
||||
<DismissibleDialog
|
||||
heading={_('Beta')}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.SmallResize}
|
||||
themeId={props.themeId}
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
@@ -76,7 +79,7 @@ const WebBetaButton: React.FC<Props> = props => {
|
||||
{renderParagraph('Feel free to use it and let us know if have any questions or notice any issues!')}
|
||||
<View style={styles.feedbackContainer}>
|
||||
<LinkButton onPress={onReportBug}>{'Report bug'}</LinkButton>
|
||||
<LinkButton onPress={onLeaveFeedback}>{'Give feedback'}</LinkButton>
|
||||
<PrimaryButton onPress={onLeaveFeedback}>{'Give feedback'}</PrimaryButton>
|
||||
</View>
|
||||
</DismissibleDialog>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import Svg, { SvgProps, G, Path, Defs, LinearGradient, Stop, ClipPath, Rect } from 'react-native-svg';
|
||||
|
||||
const JoplinCloudIcon: React.FC<SvgProps> = props => {
|
||||
return <Svg
|
||||
viewBox='0 0 84 84'
|
||||
fill='none'
|
||||
{...props}
|
||||
>
|
||||
<G clipPath='url(#a)'>
|
||||
<Path fill='url(#b)' d='M0 0h84v84H0z'/>
|
||||
<Path
|
||||
fill='#fff'
|
||||
d='M73.706 49.825c0 3.732-1.534 7.148-4.007 9.592a13.714 13.714 0 0 1-9.675 3.973h-8.199v-7.065h8.2c1.818 0 3.436-.723 4.635-1.904 1.19-1.188 1.92-2.784 1.92-4.596 0-1.804-.73-3.408-1.92-4.597a6.539 6.539 0 0 0-4.636-1.903h-6.933l.386-3.882c.042-.4.059-.79.059-1.197 0-3.3-1.342-6.251-3.513-8.412a11.964 11.964 0 0 0-8.484-3.483c-3.328 0-6.304 1.33-8.484 3.483-2.18 2.16-3.513 5.112-3.513 8.412 0 .399.017.798.06 1.197l.385 3.882h-6.154c-1.819 0-3.437.723-4.636 1.903-1.19 1.189-1.92 2.785-1.92 4.597 0 1.803.73 3.408 1.92 4.596a6.539 6.539 0 0 0 4.636 1.904h9.935a8.854 8.854 0 0 0 4.82-2.452 8.705 8.705 0 0 0 2.59-6.201v-7.523h-7.217v-2.726c0-2.32 1.903-4.215 4.25-4.215h9.968v14.464c0 4.14-1.685 8.187-4.636 11.105-2.6 2.585-6.078 4.19-9.733 4.53l-.923.083h-9.062c-3.764 0-7.21-1.52-9.674-3.973a13.483 13.483 0 0 1 0-19.185 13.673 13.673 0 0 1 8.35-3.906c.452-4.464 2.48-8.487 5.507-11.488a19.154 19.154 0 0 1 13.523-5.552 19.16 19.16 0 0 1 13.522 5.552 18.842 18.842 0 0 1 5.5 11.446c3.554.141 6.782 1.613 9.129 3.948a13.428 13.428 0 0 1 4.024 9.593z'
|
||||
strokeWidth={1.3}
|
||||
/>
|
||||
</G>
|
||||
<Defs>
|
||||
<LinearGradient
|
||||
id='b'
|
||||
x1={3}
|
||||
x2={78}
|
||||
y1={4}
|
||||
y2={79}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<Stop offset={0.14} stopColor='#3873DB'/>
|
||||
<Stop offset={0.974} stopColor='#163467'/>
|
||||
</LinearGradient>
|
||||
<ClipPath id='a'>
|
||||
<Rect width={84} height={84} fill='#fff' rx={20} />
|
||||
</ClipPath>
|
||||
</Defs>
|
||||
</Svg>;
|
||||
};
|
||||
|
||||
export default JoplinCloudIcon;
|
||||
138
packages/app-mobile/components/SyncWizard/SyncWizard.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon, Text } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import JoplinCloudIcon from './JoplinCloudIcon';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import CardButton from '../buttons/CardButton';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
visible: boolean;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const iconSize = 24;
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
paddingBottom: 6,
|
||||
alignItems: 'center',
|
||||
},
|
||||
subheading: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
cardContent: {
|
||||
padding: 12,
|
||||
borderRadius: 14,
|
||||
},
|
||||
syncProviderList: {
|
||||
gap: 8,
|
||||
},
|
||||
featuresList: {
|
||||
marginTop: 4,
|
||||
},
|
||||
listItem: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginVertical: 6,
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
});
|
||||
|
||||
interface SyncProviderProps {
|
||||
title: string;
|
||||
icon: ()=> React.ReactNode;
|
||||
description: string;
|
||||
onPress: ()=> void;
|
||||
featuresList: string[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const SyncProvider: React.FC<SyncProviderProps> = props => {
|
||||
return <CardButton
|
||||
disabled={props.disabled}
|
||||
onPress={props.onPress}
|
||||
testID='sync-provider-card'
|
||||
>
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.titleContainer}>
|
||||
{props.icon()}
|
||||
<Text variant='titleMedium'>{props.title}{props.disabled ? ' (Not supported)' : ''}</Text>
|
||||
</View>
|
||||
{props.description && <Text variant='bodyMedium'>{props.description}</Text>}
|
||||
<View style={styles.featuresList}>
|
||||
{props.featuresList.map((feature, index) => (
|
||||
<View key={`feature-${index}`} style={styles.listItem}>
|
||||
<Icon size={14} source='check'/><Text>{feature}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</CardButton>;
|
||||
};
|
||||
|
||||
const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
const onDismiss = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: false,
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const onSelectJoplinCloud = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('JoplinCloudLogin');
|
||||
}, [onDismiss]);
|
||||
|
||||
const onSelectOtherTarget = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('Config', { sectionName: 'sync' });
|
||||
}, [onDismiss]);
|
||||
|
||||
return <DismissibleDialog
|
||||
themeId={themeId}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
size={DialogVariant.SmallResize}
|
||||
scrollOverflow={true}
|
||||
heading={_('Sync')}
|
||||
>
|
||||
<Text variant='bodyLarge' role='heading' style={styles.subheading}>{
|
||||
_('Joplin can synchronise your notes using various providers. Select one from the list below.')
|
||||
}</Text>
|
||||
<View style={styles.syncProviderList}>
|
||||
<SyncProvider
|
||||
title={_('Joplin Cloud')}
|
||||
description={_('Joplin\'s own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.')}
|
||||
featuresList={[
|
||||
_('Sync your notes'),
|
||||
_('Publish notes to the internet'),
|
||||
_('Collaborate on notebooks with others'),
|
||||
]}
|
||||
icon={() => <JoplinCloudIcon width={iconSize} height={iconSize}/>}
|
||||
onPress={onSelectJoplinCloud}
|
||||
disabled={false}
|
||||
/>
|
||||
<SyncProvider
|
||||
title={_('Other')}
|
||||
description={_('Select one of the other supported sync targets.')}
|
||||
icon={() => <Icon size={iconSize} source='dots-horizontal-circle'/>}
|
||||
featuresList={[]}
|
||||
onPress={onSelectOtherTarget}
|
||||
disabled={false}
|
||||
/>
|
||||
</View>
|
||||
</DismissibleDialog>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
visible: state.syncWizardVisible,
|
||||
themeId: state.settings.theme,
|
||||
}))(SyncWizard);
|
||||
@@ -89,7 +89,7 @@ describe('TagEditor', () => {
|
||||
|
||||
const searchResult = screen.getByRole('button', { name: 'new tag 1' });
|
||||
fireEvent.press(searchResult);
|
||||
expect(currentTags).toEqual(['test', 'new tag 1']);
|
||||
expect(currentTags).toEqual(['new tag 1', 'test']);
|
||||
|
||||
// Manually unmount to prevent warnings
|
||||
unmount();
|
||||
@@ -115,7 +115,7 @@ describe('TagEditor', () => {
|
||||
|
||||
const addNewButton = screen.getByRole('button', { name: 'Add new' });
|
||||
fireEvent.press(addNewButton);
|
||||
expect(currentTags).toEqual(['test', 'create']);
|
||||
expect(currentTags).toEqual(['create', 'test']);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import focusView from '../utils/focusView';
|
||||
import { msleep } from '@joplin/utils/time';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
|
||||
export enum TagEditorMode {
|
||||
Large,
|
||||
@@ -150,23 +151,32 @@ interface TagsBoxProps {
|
||||
}
|
||||
|
||||
const TagsBox: React.FC<TagsBoxProps> = props => {
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = useMemo(() => {
|
||||
return getCollator(collatorLocale);
|
||||
}, [collatorLocale]);
|
||||
|
||||
const onRemoveTag = useCallback((tag: string) => {
|
||||
props.onRemoveTag(tag);
|
||||
}, [props.onRemoveTag]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (props.tags.length) {
|
||||
return props.tags.map(tag => (
|
||||
<TagCard
|
||||
key={`tag-${tag}`}
|
||||
title={tag}
|
||||
styles={props.styles}
|
||||
themeId={props.themeId}
|
||||
onRemove={onRemoveTag}
|
||||
autofocus={props.autofocusTag === tag}
|
||||
onAutoFocusComplete={props.onAutoFocusComplete}
|
||||
/>
|
||||
));
|
||||
return props.tags
|
||||
.sort((a, b) => {
|
||||
return collator.compare(a, b);
|
||||
})
|
||||
.map(tag => (
|
||||
<TagCard
|
||||
key={`tag-${tag}`}
|
||||
title={tag}
|
||||
styles={props.styles}
|
||||
themeId={props.themeId}
|
||||
onRemove={onRemoveTag}
|
||||
autofocus={props.autofocusTag === tag}
|
||||
onAutoFocusComplete={props.onAutoFocusComplete}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
return <Text
|
||||
style={props.styles.noTagsLabel}
|
||||
@@ -195,15 +205,13 @@ const TagsBox: React.FC<TagsBoxProps> = props => {
|
||||
</View>;
|
||||
};
|
||||
|
||||
const normalizeTag = (tagText: string) => tagText.trim().toLowerCase();
|
||||
|
||||
const TagEditor: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId, props.headerStyle);
|
||||
|
||||
const comboBoxItems = useMemo(() => {
|
||||
return props.allTags
|
||||
// Exclude tags already associated with the note
|
||||
.filter(tag => !props.tags.includes(tag.title))
|
||||
.filter(tag => !props.tags.some(o => o.toLowerCase() === tag.title?.toLowerCase()))
|
||||
.map((tag): Option => {
|
||||
const title = tag.title ?? 'Untitled';
|
||||
return {
|
||||
@@ -223,11 +231,13 @@ const TagEditor: React.FC<Props> = props => {
|
||||
|
||||
const onAddTag = useCallback((title: string) => {
|
||||
AccessibilityInfo.announceForAccessibility(_('Added tag: %s', title));
|
||||
props.onTagsChange([...props.tags, normalizeTag(title)]);
|
||||
props.onTagsChange([...props.tags, title.trim()]);
|
||||
}, [props.tags, props.onTagsChange]);
|
||||
|
||||
const onRemoveTag = useCallback(async (title: string) => {
|
||||
const previousTagIndex = props.tags.indexOf(title);
|
||||
if (!title) return;
|
||||
const lowercaseTitle = title.toLowerCase();
|
||||
const previousTagIndex = props.tags.findIndex(item => item.toLowerCase() === lowercaseTitle);
|
||||
const targetTag = props.tags[previousTagIndex + 1] ?? props.tags[previousTagIndex - 1];
|
||||
setAutofocusTag(targetTag);
|
||||
|
||||
@@ -235,7 +245,7 @@ const TagEditor: React.FC<Props> = props => {
|
||||
// prevent focus from occasionally jumping away from the tag box.
|
||||
await msleep(100);
|
||||
AccessibilityInfo.announceForAccessibility(_('Removed tag: %s', title));
|
||||
props.onTagsChange(props.tags.filter(tag => tag !== title));
|
||||
props.onTagsChange(props.tags.filter(tag => tag.toLowerCase() !== lowercaseTitle));
|
||||
}, [props.tags, props.onTagsChange]);
|
||||
|
||||
const onComboBoxSelect = useCallback((item: { title: string }) => {
|
||||
@@ -243,16 +253,16 @@ const TagEditor: React.FC<Props> = props => {
|
||||
return { willRemove: true };
|
||||
}, [onAddTag]);
|
||||
|
||||
const allTagsSet = useMemo(() => {
|
||||
const allTagsSetNormalized = useMemo(() => {
|
||||
return new Set([
|
||||
...props.allTags.map(tag => tag.title),
|
||||
...props.tags,
|
||||
...props.allTags.map(tag => tag.title?.trim()?.toLowerCase()),
|
||||
...props.tags.map(tag => tag.trim().toLowerCase()),
|
||||
]);
|
||||
}, [props.allTags, props.tags]);
|
||||
|
||||
const onCanAddTag = useCallback((tag: string) => {
|
||||
return !allTagsSet.has(normalizeTag(tag));
|
||||
}, [allTagsSet]);
|
||||
return !allTagsSetNormalized.has(tag.trim().toLowerCase());
|
||||
}, [allTagsSetNormalized]);
|
||||
|
||||
const showAssociatedTags = props.mode === TagEditorMode.Large || props.tags.length > 0;
|
||||
|
||||
|
||||
72
packages/app-mobile/components/buttons/CardButton.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { Card, TouchableRipple } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, ViewStyle } from 'react-native';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled,
|
||||
Installing,
|
||||
Installed,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onPress: ()=> void;
|
||||
disabled: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const useStyles = (disabled: boolean) => {
|
||||
return useMemo(() => {
|
||||
// For the TouchableRipple to work on Android, the card needs a transparent background.
|
||||
const baseCard = { backgroundColor: 'transparent' };
|
||||
return StyleSheet.create({
|
||||
cardOuterWrapper: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cardInnerWrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
card: disabled ? {
|
||||
...baseCard,
|
||||
opacity: 0.7,
|
||||
} : baseCard,
|
||||
content: {
|
||||
gap: 5,
|
||||
},
|
||||
});
|
||||
}, [disabled]);
|
||||
};
|
||||
|
||||
|
||||
const CardButton: React.FC<Props> = props => {
|
||||
const containerIsButton = !!props.onPress;
|
||||
const styles = useStyles(props.disabled);
|
||||
|
||||
const CardWrapper = containerIsButton ? TouchableRipple : View;
|
||||
return (
|
||||
<View style={[styles.cardOuterWrapper, props.style]}>
|
||||
<CardWrapper
|
||||
accessibilityRole={containerIsButton ? 'button' : null}
|
||||
accessible={containerIsButton}
|
||||
onPress={props.onPress}
|
||||
disabled={props.disabled}
|
||||
style={styles.cardInnerWrapper}
|
||||
testID={props.testID}
|
||||
>
|
||||
<Card
|
||||
mode='outlined'
|
||||
style={styles.card}
|
||||
>
|
||||
{props.children}
|
||||
</Card>
|
||||
</CardWrapper>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardButton;
|
||||
@@ -12,7 +12,7 @@ import PluginUserWebView from './PluginUserWebView';
|
||||
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../../../components/DismissibleDialog';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
interface Props {
|
||||
@@ -164,7 +164,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
size={DialogSize.Large}
|
||||
size={DialogVariant.Large}
|
||||
onDismiss={onClose}
|
||||
>
|
||||
{renderTabContent()}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import BackButtonService from '../../../services/BackButtonService';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import ScreenHeader from '../../ScreenHeader';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import BaseScreenComponent from '../../base-screen';
|
||||
@@ -60,6 +61,7 @@ interface ConfigScreenProps {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navigation: any;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, ConfigScreenState> {
|
||||
@@ -126,6 +128,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
void NavService.go('EncryptionConfig');
|
||||
};
|
||||
|
||||
private onShowSyncWizard_ = () => {
|
||||
this.props.dispatch({
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: true,
|
||||
});
|
||||
};
|
||||
|
||||
private saveButton_press = async () => {
|
||||
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
|
||||
if (Platform.OS === 'android') {
|
||||
@@ -231,11 +240,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
// Not implemented yet
|
||||
return true;
|
||||
}
|
||||
return await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
||||
return await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { rationale: {
|
||||
title: _('Information'),
|
||||
message: _('In order to use file system synchronisation your permission to write to external storage is required.'),
|
||||
buttonPositive: _('OK'),
|
||||
});
|
||||
} });
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
@@ -545,6 +554,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
}
|
||||
|
||||
if (section.name === 'sync') {
|
||||
addSettingButton('sync_wizard_button', _('Open Sync Wizard...'), this.onShowSyncWizard_);
|
||||
addSettingButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Card, Text, TouchableRipple } from 'react-native-paper';
|
||||
import { Card, Text } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import ActionButton from '../buttons/ActionButton';
|
||||
@@ -7,11 +7,12 @@ import { ButtonType } from '../../../../buttons/TextButton';
|
||||
import PluginChips from './PluginChips';
|
||||
import { UpdateState } from '../utils/useUpdateState';
|
||||
import { PluginCallback } from '../utils/usePluginCallbacks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import InstallButton from '../buttons/InstallButton';
|
||||
import PluginTitle from './PluginTitle';
|
||||
import RecommendedBadge from './RecommendedBadge';
|
||||
import CardButton from '../../../../buttons/CardButton';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled,
|
||||
@@ -38,28 +39,14 @@ interface Props {
|
||||
onShowPluginInfo?: PluginCallback;
|
||||
}
|
||||
|
||||
const useStyles = (compatible: boolean) => {
|
||||
return useMemo(() => {
|
||||
// For the TouchableRipple to work on Android, the card needs a transparent background.
|
||||
const baseCard = { backgroundColor: 'transparent' };
|
||||
return StyleSheet.create({
|
||||
cardContainer: {
|
||||
margin: 0,
|
||||
marginTop: 8,
|
||||
padding: 0,
|
||||
borderRadius: 14,
|
||||
},
|
||||
card: !compatible ? {
|
||||
...baseCard,
|
||||
opacity: 0.7,
|
||||
} : baseCard,
|
||||
content: {
|
||||
gap: 5,
|
||||
},
|
||||
});
|
||||
}, [compatible]);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
gap: 5,
|
||||
},
|
||||
cardContainer: {
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const PluginBox: React.FC<Props> = props => {
|
||||
const manifest = props.item.manifest;
|
||||
@@ -78,46 +65,36 @@ const PluginBox: React.FC<Props> = props => {
|
||||
props.onShowPluginInfo?.({ item: props.item });
|
||||
}, [props.onShowPluginInfo, props.item]);
|
||||
|
||||
const styles = useStyles(props.isCompatible);
|
||||
|
||||
const CardWrapper = props.onShowPluginInfo ? TouchableRipple : View;
|
||||
const containerIsButton = !!props.onShowPluginInfo;
|
||||
return (
|
||||
<CardWrapper
|
||||
accessibilityRole={containerIsButton ? 'button' : null}
|
||||
accessible={containerIsButton}
|
||||
onPress={props.onShowPluginInfo ? onPress : null}
|
||||
<CardButton
|
||||
style={styles.cardContainer}
|
||||
onPress={props.onShowPluginInfo ? onPress : null}
|
||||
testID='plugin-card'
|
||||
disabled={!props.isCompatible}
|
||||
>
|
||||
<Card
|
||||
mode='outlined'
|
||||
style={styles.card}
|
||||
testID='plugin-card'
|
||||
>
|
||||
<Card.Content style={styles.content}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<PluginTitle manifest={item.manifest} />
|
||||
<Text numberOfLines={2}>{manifest.description}</Text>
|
||||
</View>
|
||||
<RecommendedBadge manifest={item.manifest} isCompatible={props.isCompatible} themeId={props.themeId} />
|
||||
<Card.Content style={styles.content}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<PluginTitle manifest={item.manifest} />
|
||||
<Text numberOfLines={2}>{manifest.description}</Text>
|
||||
</View>
|
||||
<PluginChips
|
||||
themeId={props.themeId}
|
||||
item={props.item}
|
||||
showInstalledChip={props.showInstalledChip}
|
||||
hasErrors={props.hasErrors}
|
||||
canUpdate={props.updateState === UpdateState.CanUpdate}
|
||||
onShowPluginLog={props.onShowPluginLog}
|
||||
isCompatible={props.isCompatible}
|
||||
/>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
{props.onAboutPress ? aboutButton : null}
|
||||
{props.onInstall ? installButton : null}
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</CardWrapper>
|
||||
<RecommendedBadge manifest={item.manifest} isCompatible={props.isCompatible} themeId={props.themeId} />
|
||||
</View>
|
||||
<PluginChips
|
||||
themeId={props.themeId}
|
||||
item={props.item}
|
||||
showInstalledChip={props.showInstalledChip}
|
||||
hasErrors={props.hasErrors}
|
||||
canUpdate={props.updateState === UpdateState.CanUpdate}
|
||||
onShowPluginLog={props.onShowPluginLog}
|
||||
isCompatible={props.isCompatible}
|
||||
/>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
{props.onAboutPress ? aboutButton : null}
|
||||
{props.onInstall ? installButton : null}
|
||||
</Card.Actions>
|
||||
</CardButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { Card, Divider, List, Portal, Switch, Text } from 'react-native-paper';
|
||||
import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl';
|
||||
import { Linking, ScrollView, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import DismissibleDialog, { DialogSize } from '../../../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../../../DismissibleDialog';
|
||||
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import PluginTitle from './PluginBox/PluginTitle';
|
||||
@@ -253,7 +253,7 @@ const PluginInfoModal: React.FC<Props> = props => {
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
onDismiss={props.onModalDismiss}
|
||||
>
|
||||
{ props.item ? <PluginInfoModalContent {...props}/> : null }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../../
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import { resetRepoApi } from './utils/useRepoApi';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../../../utils/types';
|
||||
@@ -59,7 +59,7 @@ describe('PluginStates.installed', () => {
|
||||
mockPluginServiceSetup(reduxStore);
|
||||
resetRepoApi();
|
||||
|
||||
await mockMobilePlatform('android');
|
||||
await mockMobilePlatform(MobilePlatform.Android);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
// Fake timers are necessary to prevent a warning.
|
||||
@@ -73,8 +73,8 @@ describe('PluginStates.installed', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
'android',
|
||||
'ios',
|
||||
MobilePlatform.Android,
|
||||
MobilePlatform.Ios,
|
||||
])('should not allow updating a plugin that is not recommended on iOS, but should on Android (on %s)', async (platform) => {
|
||||
await mockMobilePlatform(platform);
|
||||
expect(shim.mobilePlatform()).toBe(platform);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Store } from 'redux';
|
||||
import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor';
|
||||
import { resetRepoApi } from './utils/useRepoApi';
|
||||
import mockPluginServiceSetup from '../../../../utils/testing/mockPluginServiceSetup';
|
||||
import { MobilePlatform } from '@joplin/lib/shim';
|
||||
|
||||
const expectSearchResultCountToBe = async (count: number) => {
|
||||
await waitFor(() => {
|
||||
@@ -38,7 +39,7 @@ describe('PluginStates.search', () => {
|
||||
await switchClient(0);
|
||||
reduxStore = createMockReduxStore();
|
||||
mockPluginServiceSetup(reduxStore);
|
||||
mockMobilePlatform('android');
|
||||
mockMobilePlatform(MobilePlatform.Android);
|
||||
resetRepoApi();
|
||||
|
||||
await mockRepositoryApiConstructor();
|
||||
@@ -70,7 +71,7 @@ describe('PluginStates.search', () => {
|
||||
|
||||
it('should only show recommended plugin search results on iOS-like environments', async () => {
|
||||
// iOS uses restricted install mode
|
||||
mockMobilePlatform('ios');
|
||||
mockMobilePlatform(MobilePlatform.Ios);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ItemEvent } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import { Linking } from 'react-native';
|
||||
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
|
||||
|
||||
const openWebsiteForPlugin = ({ item }: ItemEvent) => {
|
||||
return Linking.openURL(`https://joplinapp.org/plugins/plugin/${item.manifest.id}`);
|
||||
return Linking.openURL(getPluginHelpUrl(item.manifest.id));
|
||||
};
|
||||
|
||||
export default openWebsiteForPlugin;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Portal, ProgressBar, Snackbar } from 'react-native-paper';
|
||||
import useBackHandler from '../../../utils/hooks/useBackHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { ResourceOcrDriverId, ResourceOcrStatus } from '@joplin/lib/services/database/types';
|
||||
|
||||
const logger = Logger.create('DocumentScanner');
|
||||
|
||||
@@ -77,13 +78,24 @@ const DocumentScanner: React.FC<Props> = ({ themeId, dispatch }) => {
|
||||
const onCreateNote = useCallback(async (event: CreateNoteEvent) => {
|
||||
setSnackbarMessage(_('Creating note "%s"...', event.title));
|
||||
setCreatingNote(true);
|
||||
logger.info('Creating note', event.queueForTranscription ? '(with transcription)' : '');
|
||||
|
||||
try {
|
||||
const resources = [];
|
||||
for (const image of photos) {
|
||||
resources.push(await shim.createResourceFromPath(
|
||||
image.uri,
|
||||
{ title: event.title, mime: image.type },
|
||||
{
|
||||
...(event.queueForTranscription ? {
|
||||
ocr_status: ResourceOcrStatus.Todo,
|
||||
ocr_driver_id: ResourceOcrDriverId.HandwrittenText,
|
||||
ocr_details: '',
|
||||
ocr_error: '',
|
||||
ocr_text: '',
|
||||
} : {}),
|
||||
title: event.title,
|
||||
mime: image.type,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { themeStyle } from '../../global-style';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, TextStyle, View } from 'react-native';
|
||||
import { CameraResult } from '../../CameraView/types';
|
||||
import TextInput from '../../TextInput';
|
||||
import PhotoPreview from '../../CameraView/PhotoPreview';
|
||||
@@ -15,11 +15,14 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { formatMsToLocal } from '@joplin/utils/time';
|
||||
import { PrimaryButton } from '../../buttons';
|
||||
import { Switch, Text } from 'react-native-paper';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
|
||||
export interface CreateNoteEvent {
|
||||
title: string;
|
||||
tags: string[];
|
||||
parentId: string;
|
||||
queueForTranscription: boolean;
|
||||
}
|
||||
|
||||
type OnCreateNote = (event: CreateNoteEvent)=> void;
|
||||
@@ -31,6 +34,8 @@ interface Props {
|
||||
allTags: TagEntity[];
|
||||
allFolders: FolderEntity[];
|
||||
selectedFolderId: string;
|
||||
isJoplinServer: boolean;
|
||||
queueForTranscriptionDefault: boolean;
|
||||
|
||||
onCreateNote: null|OnCreateNote;
|
||||
}
|
||||
@@ -38,6 +43,11 @@ interface Props {
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const headerStyle: TextStyle = {
|
||||
...theme.headerStyle,
|
||||
fontSize: theme.fontSize,
|
||||
fontWeight: 'normal',
|
||||
};
|
||||
return StyleSheet.create({
|
||||
titleInput: {
|
||||
color: theme.color,
|
||||
@@ -58,9 +68,7 @@ const useStyles = (themeId: number) => {
|
||||
tagEditor: {
|
||||
marginHorizontal: theme.margin,
|
||||
},
|
||||
tagEditorHeader: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
tagEditorHeader: headerStyle,
|
||||
folderPickerLine: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@@ -75,6 +83,24 @@ const useStyles = (themeId: number) => {
|
||||
alignSelf: 'flex-end',
|
||||
margin: theme.margin,
|
||||
},
|
||||
transcriptionCheckboxContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginHorizontal: theme.margin,
|
||||
marginTop: theme.margin * 2,
|
||||
marginBottom: theme.margin * 2,
|
||||
gap: 6,
|
||||
},
|
||||
transcriptionCheckbox: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
transcriptionLabel: headerStyle,
|
||||
transcriptionHelp: {
|
||||
color: theme.colorFaded,
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
@@ -85,12 +111,13 @@ const tagSearchResultsProps = {
|
||||
};
|
||||
|
||||
const NotePreview: React.FC<Props> = ({
|
||||
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId,
|
||||
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId, isJoplinServer, queueForTranscriptionDefault,
|
||||
}) => {
|
||||
const styles = useStyles(themeId);
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState([]);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState(propsSelectedFolderId);
|
||||
const [queueForTranscription, setQueueForTranscription] = useState(queueForTranscriptionDefault);
|
||||
|
||||
const realFolders = useMemo(() => {
|
||||
return Folder.getRealFolders(allFolders);
|
||||
@@ -115,18 +142,33 @@ const NotePreview: React.FC<Props> = ({
|
||||
const onNewNote = useCallback(() => {
|
||||
if (!onCreateNote) return;
|
||||
|
||||
Setting.setValue('scanner.requestTranscription', queueForTranscription);
|
||||
|
||||
onCreateNote({
|
||||
tags,
|
||||
title,
|
||||
parentId: selectedFolderId ?? '',
|
||||
queueForTranscription,
|
||||
});
|
||||
}, [onCreateNote, tags, title, selectedFolderId]);
|
||||
}, [onCreateNote, tags, title, selectedFolderId, queueForTranscription]);
|
||||
|
||||
const onNewFolder = useCallback(async (title: string) => {
|
||||
const folder = await Folder.save({ title });
|
||||
setSelectedFolderId(folder.id);
|
||||
}, []);
|
||||
|
||||
const transcriptionCheckbox = <View style={styles.transcriptionCheckboxContainer}>
|
||||
<View style={styles.transcriptionCheckbox}>
|
||||
<Text nativeID='transcriptionLabel' style={styles.transcriptionLabel}>{_('Recognise text:')}</Text>
|
||||
<Switch accessibilityLabelledBy='transcriptionLabel' value={queueForTranscription} onValueChange={setQueueForTranscription} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.transcriptionHelp}>{
|
||||
_('When enabled, requests that the images in the note be transcribed with a higher-quality on-server transcription service. Requires sync with a copy of the desktop app.')
|
||||
}</Text>
|
||||
</View>
|
||||
</View>;
|
||||
|
||||
return <ScrollView style={styles.rootScrollView}>
|
||||
<TextInput
|
||||
style={styles.titleInput}
|
||||
@@ -162,6 +204,7 @@ const NotePreview: React.FC<Props> = ({
|
||||
headerStyle={styles.tagEditorHeader}
|
||||
searchResultProps={tagSearchResultsProps}
|
||||
/>
|
||||
{isJoplinServer ? transcriptionCheckbox : null}
|
||||
<PrimaryButton
|
||||
onPress={onNewNote}
|
||||
style={styles.actionButton}
|
||||
@@ -174,5 +217,7 @@ export default connect((state: AppState) => ({
|
||||
allTags: state.tags,
|
||||
allFolders: state.folders,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
isJoplinServer: SyncTargetRegistry.isJoplinServerOrCloud(state.settings['sync.target']),
|
||||
themeId: state.settings.theme,
|
||||
queueForTranscriptionDefault: state.settings['scanner.requestTranscription'],
|
||||
}))(NotePreview);
|
||||
|
||||
@@ -73,6 +73,9 @@ import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import { SelectionRange } from '../../../contentScripts/markdownEditorBundle/types';
|
||||
import { EditorType } from '../../NoteEditor/types';
|
||||
import { IconButton } from 'react-native-paper';
|
||||
import { writeTextToCacheFile } from '../../../utils/ShareUtils';
|
||||
import shareFile from '../../../utils/shareFile';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@@ -148,6 +151,7 @@ interface State {
|
||||
};
|
||||
|
||||
showSpeechToTextDialog: boolean;
|
||||
multiline: boolean;
|
||||
}
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent<State> {
|
||||
@@ -219,6 +223,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
},
|
||||
|
||||
showSpeechToTextDialog: false,
|
||||
multiline: false,
|
||||
};
|
||||
|
||||
this.titleTextFieldRef = React.createRef();
|
||||
@@ -508,7 +513,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
flexDirection: 'row',
|
||||
flexBasis: 'auto',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
};
|
||||
@@ -541,8 +545,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
if (Platform.OS === 'web') return;
|
||||
|
||||
const response = await checkPermissions(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, {
|
||||
message: _('In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.'),
|
||||
title: _('Permission needed'),
|
||||
onRequestConfirmation: async () => {
|
||||
const yesIndex = 0;
|
||||
const result = await shim.showMessageBox(
|
||||
_('Joplin supports saving the location at which notes are saved or created. Do you want to enable it? This can be changed at any time in settings.'),
|
||||
{
|
||||
buttons: [_('Yes'), _('No')],
|
||||
title: _('Save geolocation?'),
|
||||
},
|
||||
);
|
||||
return result === yesIndex;
|
||||
},
|
||||
});
|
||||
|
||||
// If the user simply pressed "Deny", we don't automatically switch it off because they might accept
|
||||
@@ -701,7 +714,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
|
||||
private title_changeText(text: string) {
|
||||
shared.noteComponent_change(this, 'title', text);
|
||||
let newText = text;
|
||||
if (Platform.OS !== 'web') {
|
||||
// Manipulating the underlying text inside of onChangeText causes issues with the cursor position jumping to the end while typing
|
||||
// when the Web app is being used on a desktop OS, so providing a toggle to expand the title field can only be done on mobile platforms
|
||||
newText = text.replace(/(\r\n|\n|\r)/gm, ' ');
|
||||
}
|
||||
shared.noteComponent_change(this, 'title', newText);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
}
|
||||
|
||||
@@ -1049,10 +1068,33 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
|
||||
private async share_onPress() {
|
||||
await Share.share({
|
||||
message: `${this.state.note.title}\n\n${this.state.note.body}`,
|
||||
title: this.state.note.title,
|
||||
});
|
||||
const shareText = `${this.state.note.title}\n\n${this.state.note.body}`;
|
||||
const filename = this.state.note.id ?? uuid.create();
|
||||
|
||||
if (shareText.length > 100000) {
|
||||
let fileToShare;
|
||||
try {
|
||||
// Using a .txt file extension causes a "No valid provider found from URL" error
|
||||
// and blank share sheet on iOS for larger log files (around 200 KiB).
|
||||
fileToShare = await writeTextToCacheFile(shareText, `${filename}.md`);
|
||||
await shareFile(fileToShare, 'text/plain');
|
||||
} catch (e) {
|
||||
logger.error('Unable to share note data:', e);
|
||||
|
||||
// Display a message to the user (e.g. in the case where the user is out of disk space).
|
||||
void shim.showErrorDialog(_('Unable to share note data. Reason: %s', e.toString()));
|
||||
} finally {
|
||||
if (fileToShare) {
|
||||
await shim.fsDriver().remove(fileToShare);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A txt extension is automatically appended to the title when shared to a file via this route
|
||||
await Share.share({
|
||||
message: shareText,
|
||||
title: filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private properties_onPress() {
|
||||
@@ -1172,69 +1214,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
await CommandService.instance().execute('attachFile', filePath);
|
||||
};
|
||||
|
||||
// private vosk_:Vosk;
|
||||
|
||||
// private async getVosk() {
|
||||
// if (this.vosk_) return this.vosk_;
|
||||
// this.vosk_ = new Vosk();
|
||||
// await this.vosk_.loadModel('model-fr-fr');
|
||||
// return this.vosk_;
|
||||
// }
|
||||
|
||||
// private async voiceRecording_onPress() {
|
||||
// logger.info('Vosk: Getting instance...');
|
||||
|
||||
// const vosk = await this.getVosk();
|
||||
|
||||
// this.voskResult_ = [];
|
||||
|
||||
// const eventHandlers: any[] = [];
|
||||
|
||||
// eventHandlers.push(vosk.onResult(e => {
|
||||
// logger.info('Vosk: result', e.data);
|
||||
// this.voskResult_.push(e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onError(e => {
|
||||
// logger.warn('Vosk: error', e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onTimeout(e => {
|
||||
// logger.warn('Vosk: timeout', e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onFinalResult(e => {
|
||||
// logger.info('Vosk: final result', e.data);
|
||||
// }));
|
||||
|
||||
// logger.info('Vosk: Starting recording...');
|
||||
|
||||
// void vosk.start();
|
||||
|
||||
// const buttonId = await dialogs.pop(this, 'Voice recording in progress...', [
|
||||
// { text: 'Stop recording', id: 'stop' },
|
||||
// { text: _('Cancel'), id: 'cancel' },
|
||||
// ]);
|
||||
|
||||
// logger.info('Vosk: Stopping recording...');
|
||||
// vosk.stop();
|
||||
|
||||
// for (const eventHandler of eventHandlers) {
|
||||
// eventHandler.remove();
|
||||
// }
|
||||
|
||||
// logger.info('Vosk: Recording stopped:', this.voskResult_);
|
||||
|
||||
// if (buttonId === 'cancel') return;
|
||||
|
||||
// const newNote: NoteEntity = { ...this.state.note };
|
||||
// newNote.body = `${newNote.body} ${this.voskResult_.join(' ')}`;
|
||||
// this.setState({ note: newNote });
|
||||
// this.scheduleSave();
|
||||
// }
|
||||
|
||||
|
||||
|
||||
public menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
@@ -1711,6 +1690,15 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
const dueDate = Note.dueDateObject(note);
|
||||
|
||||
const titleToggleButton = Platform.OS === 'web' ? null :
|
||||
<IconButton
|
||||
icon={(!this.state.multiline && 'menu-down') || (this.state.multiline && 'menu-up')}
|
||||
accessibilityLabel={(!this.state.multiline && _('Expand title')) || (this.state.multiline && _('Collapse title'))}
|
||||
onPress={() => this.setState({ multiline: !this.state.multiline })}
|
||||
size={30}
|
||||
style={{ width: 30, height: 30, alignSelf: 'center' }}
|
||||
/>;
|
||||
|
||||
const titleComp = (
|
||||
<View style={titleContainerStyle}>
|
||||
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
|
||||
@@ -1726,7 +1714,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
placeholder={_('Add title')}
|
||||
placeholderTextColor={theme.colorFaded}
|
||||
editable={!this.state.readOnly}
|
||||
multiline={this.state.multiline}
|
||||
submitBehavior = "blurAndSubmit"
|
||||
/>
|
||||
{ titleToggleButton }
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { View, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
|
||||
import { View, StyleSheet, TextInput, Platform } from 'react-native';
|
||||
import { AppState } from '../../utils/types';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import Revision from '@joplin/lib/models/Revision';
|
||||
@@ -102,15 +102,12 @@ const useStyles = (themeId: number) => {
|
||||
root: {
|
||||
...theme.rootStyle,
|
||||
},
|
||||
titleContainer: {
|
||||
titleViewContainer: {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
borderTopColor: theme.dividerColor,
|
||||
borderTopWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
titleViewContainer: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
flexBasis: 'auto',
|
||||
@@ -139,6 +136,7 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
const { note, resources } = useRevisionNote(revisions, currentRevisionId);
|
||||
const [initialScroll, setInitialScroll] = useState(0);
|
||||
const [hasRevisions, setHasRevisions] = useState(false);
|
||||
const [multiline, setMultiline] = useState(false);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const result = [];
|
||||
@@ -201,6 +199,9 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
const onHelpPress = useCallback(() => {
|
||||
void dialogs.info(helpMessageText);
|
||||
}, [helpMessageText, dialogs]);
|
||||
const onToggleTitlePress = useCallback(() => {
|
||||
void setMultiline(!multiline);
|
||||
}, [multiline]);
|
||||
|
||||
const styles = useStyles(props.themeId);
|
||||
const dropdownLabelText = _('Revision:');
|
||||
@@ -212,14 +213,25 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
>{restoreButtonTitle}</PrimaryButton>
|
||||
);
|
||||
|
||||
const titleToggleButton = Platform.OS === 'web' ? null :
|
||||
<IconButton
|
||||
icon={(!multiline && 'menu-down') || (multiline && 'menu-up')}
|
||||
accessibilityLabel={(!multiline && _('Expand title')) || (multiline && _('Collapse title'))}
|
||||
onPress={onToggleTitlePress}
|
||||
size={30}
|
||||
style={{ width: 30, height: 30, alignSelf: 'center' }}
|
||||
/>;
|
||||
|
||||
const titleComponent = (
|
||||
<SafeAreaView style={styles.titleContainer}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={styles.titleViewContainer}>
|
||||
<Text style={styles.titleText}>{note?.title ?? ''}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<View style={styles.titleViewContainer}>
|
||||
<TextInput
|
||||
style={styles.titleText}
|
||||
value={note?.title ?? ''}
|
||||
editable={false}
|
||||
multiline={multiline}
|
||||
/>
|
||||
{ titleToggleButton }
|
||||
</View>
|
||||
);
|
||||
|
||||
return <View style={styles.root}>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { render, screen } from '../../../utils/testing/testingLibrary';
|
||||
import SearchResults from './SearchResults';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import TestProviderStack from '../../testing/TestProviderStack';
|
||||
|
||||
const createNotes = async (count: number) => {
|
||||
const folder = await Folder.save({ title: 'Test Note' });
|
||||
for (let i = 0; i < count; i++) {
|
||||
await Note.save({ title: `abcd ${i}`, body: 'body', parent_id: folder.id });
|
||||
}
|
||||
await SearchEngine.instance().syncTables();
|
||||
};
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
interface WrapperProps {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
}
|
||||
const WrappedSearchResults: React.FC<WrapperProps> = props => (
|
||||
<TestProviderStack store={store}>
|
||||
<SearchResults paused={props.paused} query={props.query} onHighlightedWordsChange={() => { }} ftsEnabled={1} />
|
||||
</TestProviderStack>
|
||||
);
|
||||
|
||||
describe('SearchResult', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
});
|
||||
|
||||
test('should show results when unpaused', async () => {
|
||||
const noteCount = 8;
|
||||
await createNotes(noteCount);
|
||||
|
||||
render(<WrappedSearchResults query='abcd' paused={false}/>);
|
||||
const items = await screen.findAllByText(/abcd \d\d?\d?/);
|
||||
expect(items.length).toBe(noteCount);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
onHighlightedWordsChange: (highlightedWords: (ComplexTerm | string)[])=> void;
|
||||
|
||||
ftsEnabled: number;
|
||||
@@ -28,7 +29,7 @@ const useResults = (props: Props) => {
|
||||
let notes: NoteEntity[] = [];
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (query) {
|
||||
if (query && !props.paused) {
|
||||
if (ftsEnabled) {
|
||||
const r = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
|
||||
notes = r.notes;
|
||||
@@ -57,7 +58,7 @@ const useResults = (props: Props) => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [query, ftsEnabled], { interval: 200 });
|
||||
}, [query, props.paused, ftsEnabled], { interval: 200 });
|
||||
|
||||
return {
|
||||
notes,
|
||||
|
||||
@@ -53,11 +53,36 @@ const useStyles = (theme: ThemeStyle, visible: boolean) => {
|
||||
}, [theme, visible]);
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/laurent22/joplin/issues/12823:
|
||||
// Disable search-as-you-type for short 0-2 character searches that
|
||||
// are likely to match the start of a large number of words.
|
||||
const useSearchPaused = (query: string) => {
|
||||
const [pauseDisabled, setPauseDisabled] = useState(false);
|
||||
// Only disable search-as-you-type for a subset of all characters.
|
||||
// This is, for example, to ensure that search-as-you-type remains
|
||||
// enabled for CJK characters (e.g. U+6570 has length 1).
|
||||
const paused = query.match(/^[a-z0-9]{0,2}$/i);
|
||||
|
||||
const onOverridePause = useCallback(() => {
|
||||
setPauseDisabled(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPauseDisabled(false);
|
||||
}, [query]);
|
||||
|
||||
return {
|
||||
paused: paused && !pauseDisabled,
|
||||
onOverridePause,
|
||||
};
|
||||
};
|
||||
|
||||
const SearchScreenComponent: React.FC<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = useStyles(theme, props.visible);
|
||||
|
||||
const [query, setQuery] = useState(props.query);
|
||||
const { paused, onOverridePause } = useSearchPaused(query);
|
||||
|
||||
const globalQueryRef = useRef(props.query);
|
||||
globalQueryRef.current = props.query;
|
||||
@@ -99,6 +124,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
autoFocus={props.visible}
|
||||
underlineColorAndroid="#ffffff00"
|
||||
onChangeText={setQuery}
|
||||
onSubmitEditing={onOverridePause}
|
||||
value={query}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@@ -114,6 +140,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
|
||||
<SearchResults
|
||||
query={query}
|
||||
paused={paused}
|
||||
ftsEnabled={props.ftsEnabled}
|
||||
onHighlightedWordsChange={onHighlightedWordsChange}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,7 @@ import useEncryptionWarningMessage from '@joplin/lib/components/shared/ShareNote
|
||||
import { SharingStatus } from '@joplin/lib/components/shared/ShareNoteDialog/types';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import { themeStyle } from '../global-style';
|
||||
@@ -191,7 +191,7 @@ const ShareNoteDialog: React.FC<Props> = props => {
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
onDismiss={props.onClose}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
heading={_('Publish Note')}
|
||||
>
|
||||
{props.visible ? <ShareNoteDialogContent {...props}/> : null}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
@@ -46,13 +47,17 @@ const useStyles = (themeId: number) => {
|
||||
const TagsScreenComponent: React.FC<Props> = props => {
|
||||
const [tags, setTags] = useState<TagEntity[]>([]);
|
||||
const styles = useStyles(props.themeId);
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = useMemo(() => {
|
||||
return getCollator(collatorLocale);
|
||||
}, [collatorLocale]);
|
||||
|
||||
type TagItemPressEvent = { id: string };
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const tags = await Tag.allWithNotes();
|
||||
tags.sort((a, b) => {
|
||||
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
|
||||
return collator.compare(a.title, b.title);
|
||||
});
|
||||
setTags(tags);
|
||||
}, []);
|
||||
|
||||
@@ -96,6 +96,10 @@ const useStyles = (themeId: number) => {
|
||||
...buttonStyle,
|
||||
flex: 0,
|
||||
};
|
||||
const folderButtonTextStyle: ViewStyle = {
|
||||
...buttonTextStyle,
|
||||
paddingLeft: 0,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
menu: {
|
||||
@@ -113,9 +117,10 @@ const useStyles = (themeId: number) => {
|
||||
},
|
||||
sidebarIcon: sidebarIconStyle,
|
||||
folderButton: folderButtonStyle,
|
||||
folderButtonText: {
|
||||
...buttonTextStyle,
|
||||
paddingLeft: 0,
|
||||
folderButtonText: folderButtonTextStyle,
|
||||
conflictFolderButtonText: {
|
||||
...folderButtonTextStyle,
|
||||
color: theme.colorError,
|
||||
},
|
||||
folderButtonSelected: {
|
||||
...folderButtonStyle,
|
||||
@@ -264,6 +269,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
// depth is specified with an accessibilityLabel:
|
||||
const folderDepthDescription = props.depth > 0 ? _('(level %d)', props.depth) : '';
|
||||
const accessibilityLabel = `${folderTitle} ${folderDepthDescription}`.trim();
|
||||
const folderButtonTextStyle = props.folder.id === Folder.conflictFolderId() ? baseStyles.conflictFolderButtonText : baseStyles.folderButtonText;
|
||||
return (
|
||||
<View key={props.folder.id} style={styles.buttonWrapper}>
|
||||
<TouchableRipple
|
||||
@@ -279,7 +285,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
{renderFolderIcon(props.folder.id, folderIcon)}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={baseStyles.folderButtonText}
|
||||
style={folderButtonTextStyle}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{folderTitle}
|
||||
@@ -502,9 +508,8 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
});
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
sectionName: 'sync',
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: true,
|
||||
});
|
||||
|
||||
return 'init';
|
||||
|
||||
@@ -5,9 +5,6 @@ import { _, languageName } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import VoiceTyping, { OnTextCallback, VoiceTypingSession } from '../../services/voiceTyping/VoiceTyping';
|
||||
import whisper from '../../services/voiceTyping/whisper';
|
||||
import vosk from '../../services/voiceTyping/vosk';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { RecorderState } from './types';
|
||||
import RecordingControls from './RecordingControls';
|
||||
import { PrimaryButton } from '../buttons';
|
||||
@@ -16,19 +13,17 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
provider: string;
|
||||
onDismiss: ()=> void;
|
||||
onText: (text: string)=> void;
|
||||
}
|
||||
|
||||
interface UseVoiceTypingProps {
|
||||
locale: string;
|
||||
provider: string;
|
||||
onSetPreview: OnTextCallback;
|
||||
onText: OnTextCallback;
|
||||
}
|
||||
|
||||
const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingProps) => {
|
||||
const useVoiceTyping = ({ locale, onSetPreview, onText }: UseVoiceTypingProps) => {
|
||||
const [voiceTyping, setVoiceTyping] = useState<VoiceTypingSession>(null);
|
||||
const [error, setError] = useState<Error|null>(null);
|
||||
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
|
||||
@@ -43,8 +38,8 @@ const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypi
|
||||
voiceTypingRef.current = voiceTyping;
|
||||
|
||||
const builder = useMemo(() => {
|
||||
return new VoiceTyping(locale, provider?.startsWith('whisper') ? [whisper] : [vosk]);
|
||||
}, [locale, provider]);
|
||||
return new VoiceTyping(locale, [whisper]);
|
||||
}, [locale]);
|
||||
|
||||
const [redownloadCounter, setRedownloadCounter] = useState(0);
|
||||
|
||||
@@ -121,7 +116,6 @@ const SpeechToTextComponent: React.FC<Props> = props => {
|
||||
locale: props.locale,
|
||||
onSetPreview: setPreview,
|
||||
onText: props.onText,
|
||||
provider: props.provider,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -209,6 +203,4 @@ const SpeechToTextComponent: React.FC<Props> = props => {
|
||||
/>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
provider: state.settings['voiceTyping.preferredProvider'],
|
||||
}))(SpeechToTextComponent);
|
||||
export default SpeechToTextComponent;
|
||||
|
||||
@@ -56,13 +56,11 @@ const useWebViewSetup = ({
|
||||
})})
|
||||
` : '';
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
if (typeof markdownEditorBundle === 'undefined') {
|
||||
${shim.injectedJs('markdownEditorBundle')};
|
||||
window.markdownEditorBundle = markdownEditorBundle;
|
||||
markdownEditorBundle.setUpLogger();
|
||||
}
|
||||
|
||||
const afterLoadFinishedJs = useRef((): string => '');
|
||||
// Store as a callback to avoid rebuilding the string on each content change.
|
||||
// Since the editor content is included in editorOptions, for large documents,
|
||||
// creating the initial injected JS is potentially expensive.
|
||||
afterLoadFinishedJs.current = () => `
|
||||
if (!window.cm) {
|
||||
const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)};
|
||||
const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0;
|
||||
@@ -74,6 +72,7 @@ const useWebViewSetup = ({
|
||||
window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)});
|
||||
|
||||
${jumpToHashJs}
|
||||
|
||||
// Set the initial selection after jumping to the header -- the initial selection,
|
||||
// if specified, should take precedence.
|
||||
${setInitialSelectionJs}
|
||||
@@ -86,7 +85,15 @@ const useWebViewSetup = ({
|
||||
console.log('No parent element found with class name ', parentClassName);
|
||||
}
|
||||
}
|
||||
`, [jumpToHashJs, setInitialSearchJs, setInitialSelectionJs, editorOptions]);
|
||||
`;
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
if (typeof markdownEditorBundle === 'undefined') {
|
||||
${shim.injectedJs('markdownEditorBundle')};
|
||||
window.markdownEditorBundle = markdownEditorBundle;
|
||||
markdownEditorBundle.setUpLogger();
|
||||
}
|
||||
`, []);
|
||||
|
||||
// Scroll to the new hash, if it changes.
|
||||
const isFirstScrollRef = useRef(true);
|
||||
@@ -158,13 +165,14 @@ const useWebViewSetup = ({
|
||||
const webViewEventHandlers = useMemo(() => {
|
||||
return {
|
||||
onLoadEnd: () => {
|
||||
webviewRef.current?.injectJS(afterLoadFinishedJs.current());
|
||||
editorMessenger.onWebViewLoaded();
|
||||
},
|
||||
onMessage: (event: OnMessageEvent) => {
|
||||
editorMessenger.onWebViewMessage(event);
|
||||
},
|
||||
};
|
||||
}, [editorMessenger]);
|
||||
}, [editorMessenger, webviewRef]);
|
||||
|
||||
const api = useMemo(() => {
|
||||
return editorMessenger.remoteApi;
|
||||
|
||||
@@ -137,7 +137,11 @@ const useTempDirPath = () => {
|
||||
return tempDirPath;
|
||||
};
|
||||
|
||||
const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
|
||||
type Result = SetUpResult<RendererControl> & {
|
||||
hasPluginScripts: boolean;
|
||||
};
|
||||
|
||||
const useWebViewSetup = (props: Props): Result => {
|
||||
const tempDirPath = useTempDirPath();
|
||||
const { css, injectedJs } = useSource(tempDirPath);
|
||||
const { editPopupCss, createEditPopupSyntax, destroyEditPopupSyntax } = useEditPopup(props.themeId);
|
||||
@@ -269,6 +273,7 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
|
||||
};
|
||||
}, [createEditPopupSyntax, destroyEditPopupSyntax, messenger]);
|
||||
|
||||
const hasPluginScripts = contentScripts.length > 0;
|
||||
return useMemo(() => {
|
||||
return {
|
||||
api: rendererControl,
|
||||
@@ -280,8 +285,9 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
|
||||
onLoadEnd: messenger.onWebViewLoaded,
|
||||
onMessage: messenger.onWebViewMessage,
|
||||
},
|
||||
hasPluginScripts,
|
||||
};
|
||||
}, [css, injectedJs, messenger, editPopupCss, rendererControl]);
|
||||
}, [css, injectedJs, messenger, editPopupCss, rendererControl, hasPluginScripts]);
|
||||
};
|
||||
|
||||
export default useWebViewSetup;
|
||||
|
||||
@@ -98,7 +98,7 @@ export const initialize = async (
|
||||
},
|
||||
});
|
||||
});
|
||||
editor.setSearchState(initialSearch);
|
||||
editor.setSearchState(initialSearch, 'initialSearch');
|
||||
|
||||
messenger.setLocalInterface({
|
||||
editor,
|
||||
|
||||
@@ -44,11 +44,14 @@ const useMessenger = (props: UseMessengerProps) => {
|
||||
onAttachRef.current = props.onAttachFile;
|
||||
|
||||
const markupRenderingSettings = useRef<RenderOptions>(null);
|
||||
const baseTheme = props.settings.themeData;
|
||||
markupRenderingSettings.current = {
|
||||
themeId: props.themeId,
|
||||
highlightedKeywords: [],
|
||||
resources: props.noteResources,
|
||||
themeOverrides: {},
|
||||
themeOverrides: {
|
||||
noteViewerFontSize: `${baseTheme.fontSize}${baseTheme.fontSizeUnits ?? 'px'}`,
|
||||
},
|
||||
noteHash: '',
|
||||
initialScroll: 0,
|
||||
pluginAssetContainerSelector: null,
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
||||
@@ -363,7 +364,6 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
@@ -373,6 +373,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
||||
@@ -395,7 +396,6 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -544,7 +544,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.4.3;
|
||||
MARKETING_VERSION = 13.5.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -578,7 +578,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.4.3;
|
||||
MARKETING_VERSION = 13.5.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -782,7 +782,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.4.3;
|
||||
MARKETING_VERSION = 13.5.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
@@ -825,7 +825,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.4.3;
|
||||
MARKETING_VERSION = 13.5.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -1406,7 +1406,7 @@ PODS:
|
||||
- React-jsiexecutor
|
||||
- React-RCTFBReactNativeSpec
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-alarm-notification (3.4.0):
|
||||
- react-native-alarm-notification (3.5.0):
|
||||
- React
|
||||
- react-native-document-picker (10.1.3):
|
||||
- DoubleConversion
|
||||
@@ -1458,7 +1458,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-get-random-values (1.11.0):
|
||||
- React-Core
|
||||
- react-native-image-picker (8.0.0):
|
||||
- react-native-image-picker (8.2.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1486,7 +1486,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-quick-crypto (0.7.13):
|
||||
- react-native-quick-crypto (0.7.17):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1514,7 +1514,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (3.4.1):
|
||||
- react-native-saf-x (3.5.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.4.1):
|
||||
- React-Core
|
||||
@@ -1522,7 +1522,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.13.5):
|
||||
- react-native-webview (13.14.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1870,11 +1870,11 @@ PODS:
|
||||
- React-utils (= 0.79.2)
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
- RNCClipboard (1.16.2):
|
||||
- RNCClipboard (1.16.3):
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (8.3.0):
|
||||
- RNDateTimePicker (8.4.2):
|
||||
- React-Core
|
||||
- RNDeviceInfo (14.0.4):
|
||||
- React-Core
|
||||
@@ -1884,7 +1884,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNLocalize (3.4.1):
|
||||
- RNLocalize (3.4.2):
|
||||
- React-Core
|
||||
- RNQuickAction (0.3.13):
|
||||
- React
|
||||
@@ -1914,6 +1914,8 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSVG (15.13.0):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.2.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
@@ -2055,6 +2057,7 @@ DEPENDENCIES:
|
||||
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
|
||||
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
|
||||
- RNShare (from `../node_modules/react-native-share`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
@@ -2278,6 +2281,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-securerandom"
|
||||
RNShare:
|
||||
:path: "../node_modules/react-native-share"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
RNVectorIcons:
|
||||
:path: "../node_modules/react-native-vector-icons"
|
||||
Yoga:
|
||||
@@ -2285,7 +2290,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
|
||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||
Expo: 4b1c6de7c441e1caa1918671ae0aa34d51f019a5
|
||||
@@ -2298,7 +2303,7 @@ SPEC CHECKSUMS:
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
|
||||
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
|
||||
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
||||
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
|
||||
@@ -2334,20 +2339,20 @@ SPEC CHECKSUMS:
|
||||
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
|
||||
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
|
||||
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
|
||||
react-native-alarm-notification: fd7c73a3dc15ce2d5bd9b28dfaa5aa2e25850c7b
|
||||
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
|
||||
react-native-document-picker: da39c5e4f279d39c0356dca157b98f9dc349e5bb
|
||||
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-image-picker: 922b9ba90f144b5866d07d04b0fb2b4e9ab0ed75
|
||||
react-native-image-picker: 7babe45e727db306b3f00d08c72eda3586d6e9c1
|
||||
react-native-image-resizer: 24c5d06fae2176dc0caed4b6396e02befb44064a
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-quick-crypto: 988d8d57cd720dbe218272b60775a8e0210d0b80
|
||||
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
|
||||
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
|
||||
react-native-saf-x: 3f8b52fb8160d7322161dec02a564271cc8f4138
|
||||
react-native-saf-x: 8a349c8348f43ff7c14770da4b0d618d62593346
|
||||
react-native-safe-area-context: dde2052b903c11d677c320b599c3244021c34ce8
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
|
||||
react-native-webview: 1b5778b306d4ed09d13829a6e7a6550e3c1a644a
|
||||
react-native-webview: 2d9ffd72b87cf905cdf8821d7d27d551188bac70
|
||||
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
|
||||
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
|
||||
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
|
||||
@@ -2380,17 +2385,18 @@ SPEC CHECKSUMS:
|
||||
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
|
||||
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
|
||||
rn-fetch-blob: 25612b6d6f6e980c6f17ed98ba2f58f5696a51ca
|
||||
RNCClipboard: e1d17c9d093d8129ef50b39b63a17a0e8ccd0ade
|
||||
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
|
||||
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
|
||||
RNDateTimePicker: 29264364ea7b8cc0fb355b3843cf276a4ff78966
|
||||
RNDateTimePicker: 392bdc0d6863b5de2fe9b957c82c25b6a038db29
|
||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
||||
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
|
||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
|
||||
RNLocalize: 6a87f0490f1793d7a70042e4c55eb9a1ba6dd5b4
|
||||
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
RNShare: 675e8e4a84f0137baf33057cac8f7334b0bb4b98
|
||||
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
|
||||
RNVectorIcons: d53917643fddb261b22bd6d889776f336893622b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
|
||||
|
||||
@@ -105,6 +105,13 @@ jest.mock('react-native-zip-archive', () => {
|
||||
|
||||
jest.mock('@react-native-documents/picker', () => ({ default: { } }));
|
||||
|
||||
// This is one of the icon libraries that react-native-paper attempts to use.
|
||||
// Throwing an Error causes react-native-paper to select a different icon library
|
||||
// that better supports our automated testing environment.
|
||||
jest.doMock('@expo/vector-icons/MaterialCommunityIcons', () => {
|
||||
throw new Error('Not supported in testing environments.');
|
||||
});
|
||||
|
||||
// Used by the renderer
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/app-mobile",
|
||||
"description": "Joplin for Mobile",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "BROWSERSLIST_IGNORE_OLD_DATA=true react-native start --reset-cache",
|
||||
@@ -22,18 +22,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@joplin/editor": "~3.4",
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/react-native-alarm-notification": "~3.4",
|
||||
"@joplin/react-native-saf-x": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@react-native-clipboard/clipboard": "1.16.2",
|
||||
"@react-native-community/datetimepicker": "8.3.0",
|
||||
"@joplin/editor": "~3.5",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/react-native-alarm-notification": "~3.5",
|
||||
"@joplin/react-native-saf-x": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.2",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-documents/picker": "10.1.3",
|
||||
"@react-native-documents/picker": "10.1.5",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
@@ -41,9 +41,9 @@
|
||||
"crypto-browserify": "3.12.1",
|
||||
"deprecated-react-native-prop-types": "5.0.0",
|
||||
"events": "3.3.0",
|
||||
"expo": "53.0.19",
|
||||
"expo": "53.0.20",
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.10",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
@@ -58,10 +58,10 @@
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-image-picker": "8.0.0",
|
||||
"react-native-localize": "3.4.1",
|
||||
"react-native-image-picker": "8.2.1",
|
||||
"react-native-localize": "3.4.2",
|
||||
"react-native-modal-datetime-picker": "18.0.0",
|
||||
"react-native-paper": "5.13.5",
|
||||
"react-native-paper": "5.14.5",
|
||||
"react-native-popup-menu": "0.17.0",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
@@ -70,12 +70,12 @@
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.0.11",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.2.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-zip-archive": "7.0.1",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-zip-archive": "7.0.2",
|
||||
"react-redux": "8.1.3",
|
||||
"redux": "4.2.1",
|
||||
"rn-fetch-blob": "0.12.0",
|
||||
@@ -88,41 +88,41 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/plugin-transform-export-namespace-from": "7.25.9",
|
||||
"@babel/plugin-transform-export-namespace-from": "7.27.1",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/runtime": "7.25.0",
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/turndown": "~4.0.80",
|
||||
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
||||
"@js-draw/material-icons": "1.30.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@js-draw/material-icons": "1.30.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||
"@react-native-community/cli": "16.0.3",
|
||||
"@react-native-community/cli-platform-android": "16.0.3",
|
||||
"@react-native-community/cli-platform-ios": "16.0.3",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/metro-config": "0.79.2",
|
||||
"@react-native/typescript-config": "0.79.2",
|
||||
"@react-native/babel-preset": "0.80.1",
|
||||
"@react-native/metro-config": "0.79.5",
|
||||
"@react-native/typescript-config": "0.79.5",
|
||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||
"@testing-library/react-native": "13.2.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/node": "18.19.119",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.135",
|
||||
"@types/tar-stream": "3.1.3",
|
||||
"@types/serviceworker": "0.0.142",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.20.0",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild": "0.25.6",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.2.0",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jetifier": "2.0.0",
|
||||
"js-draw": "1.30.0",
|
||||
"js-draw": "1.30.1",
|
||||
"jsdom": "26.1.0",
|
||||
"nodemon": "3.1.10",
|
||||
"punycode": "2.3.1",
|
||||
@@ -130,17 +130,17 @@
|
||||
"react-native-web": "0.20.0",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.2",
|
||||
"sharp": "0.34.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"timers-browserify": "2.0.12",
|
||||
"ts-jest": "29.3.1",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-loader": "9.5.2",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.2.1"
|
||||
"webpack-dev-server": "5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -107,6 +107,7 @@ import DocumentScanner from './components/screens/DocumentScanner/DocumentScanne
|
||||
import buildStartupTasks from './utils/buildStartupTasks';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import appReducer from './utils/appReducer';
|
||||
import SyncWizard from './components/SyncWizard/SyncWizard';
|
||||
|
||||
const logger = Logger.create('root');
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
@@ -143,7 +144,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
if (action.type === 'NAV_GO') Keyboard.dismiss();
|
||||
|
||||
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(reg.syncAsYouTypeInterval(), { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
@@ -762,6 +763,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
</View>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
|
||||
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
|
||||
<SyncWizard/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SideMenu>
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import { languageCodeOnly } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { rtrimSlashes } from '@joplin/lib/path-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Vosk from 'react-native-vosk';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { VoiceTypingProvider, VoiceTypingSession } from './VoiceTyping';
|
||||
import { join } from 'path';
|
||||
|
||||
const logger = Logger.create('voiceTyping/vosk');
|
||||
|
||||
enum State {
|
||||
Idle = 0,
|
||||
Recording,
|
||||
Completing,
|
||||
}
|
||||
|
||||
interface StartOptions {
|
||||
onResult: (text: string)=> void;
|
||||
}
|
||||
|
||||
let vosk_: Record<string, Vosk> = {};
|
||||
|
||||
let state_: State = State.Idle;
|
||||
|
||||
const defaultSupportedLanguages = {
|
||||
'en': 'https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip',
|
||||
'zh': 'https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip',
|
||||
'ru': 'https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip',
|
||||
'fr': 'https://alphacephei.com/vosk/models/vosk-model-small-fr-0.22.zip',
|
||||
'de': 'https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip',
|
||||
'es': 'https://alphacephei.com/vosk/models/vosk-model-small-es-0.42.zip',
|
||||
'pt': 'https://alphacephei.com/vosk/models/vosk-model-small-pt-0.3.zip',
|
||||
'tr': 'https://alphacephei.com/vosk/models/vosk-model-small-tr-0.3.zip',
|
||||
'vn': 'https://alphacephei.com/vosk/models/vosk-model-small-vn-0.4.zip',
|
||||
'it': 'https://alphacephei.com/vosk/models/vosk-model-small-it-0.22.zip',
|
||||
'nl': 'https://alphacephei.com/vosk/models/vosk-model-small-nl-0.22.zip',
|
||||
'uk': 'https://alphacephei.com/vosk/models/vosk-model-small-uk-v3-small.zip',
|
||||
'ja': 'https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip',
|
||||
'hi': 'https://alphacephei.com/vosk/models/vosk-model-small-hi-0.22.zip',
|
||||
'cs': 'https://alphacephei.com/vosk/models/vosk-model-small-cs-0.4-rhasspy.zip',
|
||||
'pl': 'https://alphacephei.com/vosk/models/vosk-model-small-pl-0.22.zip',
|
||||
'uz': 'https://alphacephei.com/vosk/models/vosk-model-small-uz-0.22.zip',
|
||||
'ko': 'https://alphacephei.com/vosk/models/vosk-model-small-ko-0.22.zip',
|
||||
};
|
||||
|
||||
export const isSupportedLanguage = (locale: string) => {
|
||||
const l = languageCodeOnly(locale).toLowerCase();
|
||||
return Object.keys(defaultSupportedLanguages).includes(l);
|
||||
};
|
||||
|
||||
// Where all the models files for all the languages are
|
||||
const getModelRootDir = () => {
|
||||
return `${RNFetchBlob.fs.dirs.DocumentDir}/vosk-models`;
|
||||
};
|
||||
|
||||
// Where we unzip a model after downloading it
|
||||
const getUnzipDir = (locale: string) => {
|
||||
return `${getModelRootDir()}/${locale}`;
|
||||
};
|
||||
|
||||
// Where the model for a particular language is
|
||||
const getModelDir = (locale: string) => {
|
||||
return `${getUnzipDir(locale)}/model`;
|
||||
};
|
||||
|
||||
const languageModelUrl = (locale: string): string => {
|
||||
const lang = languageCodeOnly(locale).toLowerCase();
|
||||
if (!(lang in defaultSupportedLanguages)) throw new Error(`No language file for: ${locale}`);
|
||||
|
||||
const urlTemplate = rtrimSlashes(Setting.value('voiceTypingBaseUrl').trim());
|
||||
|
||||
if (urlTemplate) {
|
||||
let url = rtrimSlashes(urlTemplate);
|
||||
if (!url.includes('{lang}')) url += '/{lang}.zip';
|
||||
return url.replace(/\{lang\}/g, lang);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return (defaultSupportedLanguages as any)[lang];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const getVosk = async (modelDir: string, locale: string) => {
|
||||
if (vosk_[locale]) return vosk_[locale];
|
||||
|
||||
const vosk = new Vosk();
|
||||
logger.info(`Loading model from ${modelDir}`);
|
||||
await shim.fsDriver().readDirStats(modelDir);
|
||||
const result = await vosk.loadModel(modelDir);
|
||||
logger.info('getVosk:', result);
|
||||
|
||||
vosk_ = { [locale]: vosk };
|
||||
|
||||
return vosk;
|
||||
};
|
||||
|
||||
export const startRecording = (vosk: Vosk, options: StartOptions): VoiceTypingSession => {
|
||||
if (state_ !== State.Idle) throw new Error('Vosk is already recording');
|
||||
|
||||
state_ = State.Recording;
|
||||
|
||||
const result: string[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const eventHandlers: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const finalResultPromiseResolve: Function = null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const finalResultPromiseReject: Function = null;
|
||||
const finalResultTimeout = false;
|
||||
|
||||
const completeRecording = (finalResult: string, error: Error) => {
|
||||
logger.info(`Complete recording. Final result: ${finalResult}. Error:`, error);
|
||||
|
||||
for (const eventHandler of eventHandlers) {
|
||||
eventHandler.remove();
|
||||
}
|
||||
|
||||
vosk.cleanup();
|
||||
|
||||
state_ = State.Idle;
|
||||
|
||||
if (error) {
|
||||
if (finalResultPromiseReject) finalResultPromiseReject(error);
|
||||
} else {
|
||||
if (finalResultPromiseResolve) finalResultPromiseResolve(finalResult);
|
||||
}
|
||||
};
|
||||
|
||||
eventHandlers.push(vosk.onResult(e => {
|
||||
const text = e.data;
|
||||
logger.info('Result', text);
|
||||
result.push(text);
|
||||
options.onResult(text);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onError(e => {
|
||||
logger.warn('Error', e.data);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onTimeout(e => {
|
||||
logger.warn('Timeout', e.data);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onFinalResult(e => {
|
||||
logger.info('Final result', e.data);
|
||||
|
||||
if (finalResultTimeout) {
|
||||
logger.warn('Got final result - but already timed out. Not doing anything.');
|
||||
return;
|
||||
}
|
||||
|
||||
completeRecording(e.data, null);
|
||||
}));
|
||||
|
||||
const stopOrCancel = () => {
|
||||
if (state_ === State.Recording) {
|
||||
logger.info('Cancelling...');
|
||||
state_ = State.Completing;
|
||||
vosk.stopOnly();
|
||||
completeRecording('', null);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
logger.info('Starting recording...');
|
||||
await vosk.start();
|
||||
},
|
||||
stop: async () => {
|
||||
stopOrCancel();
|
||||
},
|
||||
cancel: async () => {
|
||||
stopOrCancel();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const vosk: VoiceTypingProvider = {
|
||||
supported: () => true,
|
||||
modelLocalFilepath: (locale: string) => getModelDir(locale),
|
||||
deleteCachedModels: async (locale: string) => {
|
||||
const path = getModelDir(locale);
|
||||
await shim.fsDriver().remove(path, { recursive: true });
|
||||
},
|
||||
getDownloadUrl: (locale) => languageModelUrl(locale),
|
||||
getUuidPath: (locale: string) => join(getModelDir(locale), 'uuid'),
|
||||
build: async ({ callbacks, locale, modelPath }) => {
|
||||
const vosk = await getVosk(modelPath, locale);
|
||||
return startRecording(vosk, { onResult: callbacks.onFinalize });
|
||||
},
|
||||
modelName: 'vosk',
|
||||
};
|
||||
|
||||
export default vosk;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { VoiceTypingProvider } from './VoiceTyping';
|
||||
|
||||
const vosk: VoiceTypingProvider = {
|
||||
supported: () => false,
|
||||
modelLocalFilepath: () => null,
|
||||
getDownloadUrl: () => null,
|
||||
getUuidPath: () => null,
|
||||
deleteCachedModels: () => null,
|
||||
build: async () => {
|
||||
throw new Error('Unsupported!');
|
||||
},
|
||||
modelName: 'vosk',
|
||||
};
|
||||
|
||||
export default vosk;
|
||||
@@ -17,6 +17,7 @@ const appDefaultState: AppState = {
|
||||
disableSideMenuGestures: false,
|
||||
showPanelsDialog: false,
|
||||
noteEditorVisible: false,
|
||||
syncWizardVisible: false,
|
||||
...defaultState,
|
||||
|
||||
// On mobile, it's possible to select notes that aren't in the selected folder/tag/etc.
|
||||
|
||||
@@ -195,6 +195,10 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
case 'NOTE_EDITOR_VISIBLE_CHANGE':
|
||||
newState = { ...state, noteEditorVisible: action.visible };
|
||||
break;
|
||||
|
||||
case 'SYNC_WIZARD_VISIBLE_CHANGE':
|
||||
newState = { ...state, syncWizardVisible: action.visible };
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const { Platform, PermissionsAndroid } = require('react-native');
|
||||
import { Platform, PermissionsAndroid, Permission } from 'react-native';
|
||||
const logger = Logger.create('checkPermissions');
|
||||
|
||||
type rationale = {
|
||||
type Rationale = {
|
||||
title: string;
|
||||
message: string;
|
||||
buttonPositive?: string;
|
||||
buttonPositive: string;
|
||||
buttonNegative?: string;
|
||||
buttonNeutral?: string;
|
||||
};
|
||||
|
||||
export default async (permissions: string, rationale?: rationale) => {
|
||||
interface Options {
|
||||
rationale?: Rationale;
|
||||
onRequestConfirmation?: ()=> Promise<boolean>;
|
||||
}
|
||||
|
||||
export default async (permissions: Permission, { rationale, onRequestConfirmation }: Options = {}) => {
|
||||
// On iOS, permissions are prompted for by the system, so here we assume it's granted.
|
||||
if (Platform.OS !== 'android') return PermissionsAndroid.RESULTS.GRANTED;
|
||||
|
||||
let result = await PermissionsAndroid.check(permissions);
|
||||
logger.info('Checked permission:', result);
|
||||
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
||||
result = await PermissionsAndroid.request(permissions, rationale);
|
||||
const granted = await PermissionsAndroid.check(permissions);
|
||||
logger.info('Checked permission:', granted);
|
||||
if (granted) {
|
||||
return PermissionsAndroid.RESULTS.GRANTED;
|
||||
} else {
|
||||
if (onRequestConfirmation && !await onRequestConfirmation()) {
|
||||
return PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN;
|
||||
}
|
||||
|
||||
const result = await PermissionsAndroid.request(permissions, rationale);
|
||||
logger.info('Requested permission:', result);
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Platform } from 'react-native';
|
||||
|
||||
const lockToSingleInstance = async () => {
|
||||
if (Platform.OS !== 'web') return;
|
||||
if (__DEV__) return;
|
||||
|
||||
const channel = new BroadcastChannel('single-instance-lock');
|
||||
channel.postMessage('app-opened');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import shimInitShared from './shimInitShared';
|
||||
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
const { GeolocationReact } = require('../geolocation-react.js');
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { generateSecureRandom } from 'react-native-securerandom';
|
||||
@@ -165,7 +165,7 @@ export default function shimInit() {
|
||||
};
|
||||
|
||||
shim.mobilePlatform = () => {
|
||||
return Platform.OS;
|
||||
return Platform.OS as MobilePlatform;
|
||||
};
|
||||
|
||||
shim.isAppleSilicon = () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { getLocales } from 'react-native-localize';
|
||||
import type Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import { closestSupportedLocale, defaultLocale, setLocale } from '@joplin/lib/locale';
|
||||
|
||||
const shimInitShared = () => {
|
||||
@@ -76,7 +76,7 @@ const shimInitShared = () => {
|
||||
};
|
||||
|
||||
shim.mobilePlatform = () => {
|
||||
return Platform.OS;
|
||||
return Platform.OS as MobilePlatform;
|
||||
};
|
||||
|
||||
shim.platformArch = () => {
|
||||
@@ -116,8 +116,7 @@ const shimInitShared = () => {
|
||||
const resourceId = defaultProps.id ? defaultProps.id : uuid.create();
|
||||
|
||||
const ext = fileExtension(filePath);
|
||||
let mimeType = mimeUtils.fromFileExtension(ext);
|
||||
if (!mimeType) mimeType = 'image/jpeg';
|
||||
const mimeType = defaultProps.mime ?? mimeUtils.fromFileExtension(ext) ?? 'image/jpeg';
|
||||
|
||||
let resource = Resource.new();
|
||||
resource.id = resourceId;
|
||||
|
||||
@@ -11,4 +11,5 @@ export interface AppState extends State {
|
||||
noteSideMenuOptions: any;
|
||||
disableSideMenuGestures: boolean;
|
||||
noteEditorVisible: boolean;
|
||||
syncWizardVisible: boolean;
|
||||
}
|
||||
|
||||
1
packages/app-mobile/web/mocks/throwOnLoad.js
Normal file
@@ -0,0 +1 @@
|
||||
throw new Error('Failed to load');
|
||||
@@ -45,6 +45,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
|
||||
};
|
||||
|
||||
const emptyLibraryMock = path.resolve(__dirname, 'mocks/empty.js');
|
||||
const throwOnLoadLibraryMock = path.resolve(__dirname, 'mocks/throwOnLoad.js');
|
||||
|
||||
return {
|
||||
output: {
|
||||
@@ -78,6 +79,8 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
|
||||
'@react-native-documents/picker': emptyLibraryMock,
|
||||
'react-native-exit-app': emptyLibraryMock,
|
||||
'expo-camera': emptyLibraryMock,
|
||||
// Remove this after upgrading react-native-vector-icons.
|
||||
'@react-native-vector-icons/material-design-icons': throwOnLoadLibraryMock,
|
||||
|
||||
// Workaround for applying serviceworker types to a single file.
|
||||
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.
|
||||
|
||||